Lighthouse of FE biginner

[React] useRef 톺아보기 본문

[WEB] 프론트엔드

[React] useRef 톺아보기

[FE] Lighthouse 2024. 9. 7. 13:52

Overview

React 프로젝트를 하다보면 DOM을 직접 제어하거나, DOM의 정보를 필요로 하는 시점이 있습니다.

또 렌더링을 유발시키지 않는 특정 값이 필요로 하는 경우가 있을수도 있고, 자식 컴포넌트에서 정의된 함수를 상위 컴포넌트에서 사용하고 싶은 경우가 있습니다.

 

이번 포스팅에서는 위와 같은 케이스를 바탕으로 React의 useRef 훅에 대해서 살펴보겠습니다.

 

useRef

React에서 랜더링을 발생시키지 않는 값을 필요로 한다면, 즉 React에 의해서 제어받지 않는 값을 필요로 한다면 고민해볼 수 있는 것이 useRef를 활용한 ref 객체 입니다.

 

React 프로젝트를 하다보면 변하는 값, 즉 상태를 state로 정의합니다. state는 랜더링을 유발하고 React 재조정 엔진에 제어를 당합니다. 반대로 useRef를 사용해 리턴 받는 ref 객체는 값이 변화 하더라도 랜더링이 발생하지 않는 비제어 값 입니다.

 

간단하게 useRef를 사용하는 방법을 살펴봅시다!

 

먼저 useRef 훅에 파라미터로 넘겨주는 값은 useRef의 초기값이 됩니다.

ref 객체는 current 프로퍼티를 가지고 있는데, 초기 값은 ref 객체의 current 값이 됩니다.

const numRef = useRef(1);

 

위와 같이 초기 값으로 1을 넣어준 후 console로 numRef.current을 찍어본다면 1 의 값이 콘솔에 찍히게 됩니다.

 

useRef 활용하기

DOM을 직접 다뤄야 할 경우

React 프로젝트를 진행할 때 DOM을 직접 다뤄야 하는 상황이 온다면 여러분들은 어떤 선택을 하실건가요?

 

예를 들면 특정 DOM 노드에 접근했을 때 스크롤 이벤트가 발생한다던지, Element를 클릭 시 특정 DOM으로 스크롤이 발생해야 한다는 이벤트가 있을 수 있습니다.

 

이런 경우 우리는 useRef를 활용해 DOM 노드를 활용할 수 있습니다.

React에서는 DOM을 직접 핸들링 하는 것을 금지하고 있습니다. DOM을 React 재조정 엔진에 맡기지 않고 직접 핸들링 할 경우 예상치 못한 결과가 발생할 여지 (순수하지 못한 함수)가 있기 때문입니다.

 

그럼 간단한 예제를 통해 DOM을 다루는 방법을 살펴보겠습니다.

 

import { useRef } from "react";

export default function App() {
  const ref = useRef<HTMLDivElement>(null);

  return (
    <div ref={ref} />
  );
}

 

먼저 useRef hook을 선언해준 후 초기 값으로 null을 넣어줍니다. 그 이후 참조가 필요한 DOM 노드에 ref 객체를 바인딩 합니다.

import { useRef } from "react";

export default function App() {
  const ref = useRef<HTMLDivElement>(null);
  
  const handleClick = () => {
    if (ref.current) {
      console.log(ref);
    }
  }

  return (
    <div ref={ref}>
      <button onClick={handleClick}>event</button>
    </div>
  );
}

 

하나의 버튼을 두고 ref를 콘솔에 찍어보는 이벤트를 정의해봅시다. 

이때 주의해야 할 점은 초기 값을 null로 넣어줬기 때문에 current의 값을 체크해줘야 한다는 점 입니다.

 

ref 객체 전달하기

부모 컴포넌트에서 자식 컴포넌트로 ref를 전달하려면 어떻게 해야할까요?

ref는 특수한 객체이기 때문에 일반적인 props를 넘겨주는 방식으로 자식 컴포넌트에게 넘겨줄 수 없습니다.

자식 컴포넌트에서 ref를 props로 받기 위해서는 forwardRef 라는 고차 함수를 사용해야 합니다.

더보기
더보기

React 19 버전 이후로는 forwardRef를 사용하지 않아도 자식 컴포넌트에서 ref를 전달 받을 수 있습니다.

간단한 컴포넌트를 정의해 ref를 받는 연습을 해보겠습니다.

 

import { forwardRef } from "react";

type Props = {
  title: string;
};

const DomRef = forwardRef<HTMLDivElement, Props>((props, ref) => {
  return <div ref={ref}>{props.title}</div>;
});

export default DomRef;

 

DomRef라는 컴포넌트는 Props와 ref를 전달 받습니다. 전달 받은 ref는 div에 바인딩 할 예정입니다.

TypeScript를 사용하기 때문에 제네릭을 활용해 타입을 바인딩 해줘야 합니다.

제네릭의 첫 번째 인자로는 ref의 타입을 넣어줍니다. 두 번째 인자로는 Props의 타입을 넣어줍니다.

 

forwardRef의 매개변수로는 컴포넌트를 전달합니다. 첫 번째 인자로 props, 두 번째 인자로 ref를 받습니다. 이 때 주의해야 할 점은 ref는 props에 담겨있지 않다는 점 입니다. ref는 특수한 props이기 때문에 항상 두 번째 인자로 전달 받습니다.

 

import { useRef } from "react";

export default function App() {
  const ref = useRef<HTMLDivElement>(null);

  return (
    <DomRef title="title" ref={ref} />
  );
}

 

사용하는 곳에서는 props와 ref를 컴포넌트에서 정의된 대로 내려줄 수 있습니다.

 

자식 컴포넌트에서 정의한 함수 사용하기

부모 컴포넌트에서 자식 컴포넌트에 정의된 함수를 사용해야 할 경우가 있습니다.

React는 단방향으로(위에서 아래로) 데이터가 흘러가기 때문에 아래에 정의된 상태를 위로 끌어올리고 싶을 땐 특정 패턴을 활용해야 합니다.

간단한 예제를 통해서 살펴봅시다.

 

자식 컴포넌트는 input 노드가 존재하고 해당 노드는 state로 제어됩니다. 부모 컴포넌트에서는 특정 이벤트가 발생할 경우 state값을 필요로 합니다. 우리는 부모 컴포넌트에서 ref 객체를 정의해 소유하고 있고, props로 자식 컴포넌트에 내려 자식 컴포넌트에서 함수를 정의해 자식 컴포넌트의 state를 전달해 부모 컴포넌트에서 사용할 수 있도록 예제를 구현해보겠습니다.

물론 이런 케이스는 부모 컴포넌트에서 state를 매개 변수로 받는 함수를 정의하고 정의한 함수를 props로 내려주는 방식을 사용할 수 있습니다.

 

import { useEffect, useRef, useState } from "react";
import CustomRef from "./custom-ref";

export default function App() {
  const customRef = useRef<{ getTitle: () => string } | null>(null);

  return (
    <div className="App">
      <CustomRef ref={customRef} title="init value" />
    </div>
  );
}

 

먼저 ref를 정의합니다. current 프로퍼티를 가진 ref 객체에는 getTitle이라는 함수를 바인딩 할 계획입니다. 초기 값은 null로 넣어주도록 하겠습니다.

 

CustomRef 컴포넌트를 구현합시다.

import { forwardRef, useImperativeHandle, useState } from "react";

type Props = {
  title: string;
};

const CustomRef = forwardRef<{ getTitle: () => string }, Props>(
  (props, ref) => {
    const [title, setTitle] = useState(props.title);

    useImperativeHandle(ref, () => {
      return {
        getTitle: () => title,
      };
    });

    return (
      <div>
        <input
          value={title}
          onChange={(e) => setTitle(e.currentTarget.value)}
        />
      </div>
    );
  }
);

export default CustomRef;

 

title 이라는 state를 가지고 있으며 input 노드를 통해 state 값을 입력받는 컴포넌트 입니다.

부모 컴포넌트에서 ref와 Props를 전달 받았고, ref에 getTitle이라는 함수를 정의해야 합니다.

우리는 이때 useImperativeHandle 이라는 API를 사용해 ref 객체에 함수를 바인딩 할 수 있습니다.

useImperativeHandle 자세히 살펴보기

 

 

해당 훅의 첫 번째 인자로 핸들링 할 ref 객체를 넣어주세요. 두 번째 인자로는 익명 함수를 넣어주는데, return 되는 객체를 통해 ref를 정의할 수 있습니다.

우리는 간단히 getTitle이라는 함수를 정의하고, title 상태 값을 리턴하는 함수를 정의하겠습니다.

 

부모 컴포넌트에서 이를 활용해봅시다! 부모 컴포넌트에서 하나의 상태 값을 받고, 이벤트를 통해 title 값을 해당 상태 값으로 전달하겠습니다. 그리고 해당 값을 화면에 노출합니다.

import { useRef, useState } from "react";
import CustomRef from "./custom-ref";

export default function App() {
  const [input, setInput] = useState("");
  const customRef = useRef<{ getTitle: () => string } | null>(null);

  const handleGetValue = () => {
    if (customRef?.current) {
      const inputValue = customRef.current.getTitle();
      setInput(inputValue);
    }
  };

  return (
    <div className="App">
      <CustomRef ref={customRef} title="init value" />
      <button onClick={handleGetValue}>값 동기화</button>
      <div>입력된 값은 {input} 입니다.</div>
    </div>
  );
}

 

handleGetValue 라는 함수를 정의하고 button에 클릭 이벤트로 바인딩 합니다. 해당 함수에서 ref에 바인딩 된 getTitle 함수를 호출합니다. 

 

구현된 컴포넌트를 확인해보니 자식 컴포넌트의 값을 잘 가져오는 것을 확인할 수 있습니다.

 

마치며

오늘 포스팅을 통해 useRef를 활용하는 몇 가지의 케이스를 살펴봤습니다.

살펴본 케이스들은 가장 대표적인 케이스이지만 ref 객체는 다양한 활용법이 있습니다.

잘 활용한다면 프로젝트에서 요구사항을 상당히 편리하게 구현할 수 있는 장점이 있습니다.

 

읽어봐주셔서 감사합니다.