본문 바로가기
Frontend/React & Next.js

공식문서로 ref 알아보기

by 이의찬 2023. 11. 1.

1. Why?

원래 다른 글부터 쓰다가 이걸 설명해야 할 것 같아서 이 글을 먼저 작성한다.

이 남자는 블로그 글을 컴포넌트처럼 나눠드립니다.

아오 나누기 귀찮아

공식문서의 흐름을 따라서 ref로 값 참조하기, ref로 DOM 조작하기를 설명하고 그 다음 forwardRef를 볼 예정이다. 다만 아직 공식문서가 번역이 이뤄지지 않았기에 정재남님이 번역한 React 공식문서 비공식 번역판을 참고해서 글을 작성하겠다.


2. ref로 값 참조하기

4년간 철학 수업을 들으면서 느낀건 언제나 개념을 정의하는게 가장 중요하다는 것이다. ref의 개념부터 정의해보자.

ref는 reference를 의미하며, 한국말로는 참고, 참조를 뜻한다. 최신 공식문서에는 다음과 같이 적혀있다.

When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a ref.
컴포넌트가 특정 정보를 ‘기억’하도록 하고 싶지만 해당 정보가 새 렌더링을 촉발하지 않도록 하려는 경우 ref를 사용할 수 있습니다.


이 부분에서 useState와의 가장 큰 차이점을 알 수 있다. useState는 리렌더링을 일으키기 위해서 사용하지만, ref는 정반대로 리렌더링을 일으키고 싶지 않을 때 사용한다. 이 ref값을 가져다 쓰기 위해서 사용하는 것이 useRef다. 좀 더 알아보자. 

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

위의 코드를 보면 변수 ref에 useRef를 통해서 0을 할당하고, 버튼이 클릭될 때 마다 ref.current의 값을 1씩 키워주고 있다. 이 값들은 읽고 쓰기가 모두 가능하며, React에 추적되지 않기에 변경되에도 리렌더링이 일어나지 않는다!

여기서의 ref는 숫자를 가리키고 있지만, state와 마찬가지로 문자열, 객체, 함수 등 무엇이든 가리킬 수 있습니다. states와 달리 ref는 current 속성을 읽고 수정할 수 있는 일반 JavaScript 객체입니다.


일반 Js 객체라는 부분이 중요한데, 그렇기에 useState와는 다르게 setState를 사용해서 값을 수정할 필요 없이 ref.current를 이용해서 ref 값에 직접 접근이 가능하며, 비동기적으로 작동하는 state와는 다르게 즉시 값이 변경된다. 그런데 이 쯤에서 의문점이 생긴다.

 

"저기요, 그냥 변수로 선언하면 안되나요?"

 

하지만 let이나 const로 선언된 변수는 리렌더링이 일어날 때 값이 초기화된다. 그러니까 리렌더링을 일으키지도, 영향을 받지도 않는 값을 사용하고 싶을 때 ref를 사용하면 된다.

렌더링에 정보가 사용되는 경우 해당 정보를 state로 유지하세요. 이벤트 핸들러만 정보를 필요로 하고 변경해도 다시 렌더링할 필요가 없는 경우, ref를 사용하는 것이 더 효율적일 수 있습니다.


공식문서에서는 렌더링 도중에 ref를 읽거나 쓴다면, react가 ref.current값이 언제 변경되는지 추적하지 못하기에 state를 사용하는 것을 추천한다. 좀 더 자세히 알아보자.

렌더링 중에는 ref.current를 쓰거나 읽지 마세요. React는 컴포넌트의 본문이 순수 함수처럼 동작하기를 기대합니다.
입력값들(props, state, context)이 동일하면 완전히 동일한 JSX를 반환해야 합니다. 렌더링 중에 ref를 읽거나 쓰면 이러한 기대가 깨집니다.

 

즉, 렌더링 도중에 ref를 읽거나 쓰게 되면 side effect로 인해서 컴포넌트의 순수성이 깨질 수 있다. 그러니까 렌더링 도중에 값을 사용하려면 마음 편하게 사용할 수 있는 state를 쓰고, 대신 이벤트 핸들러나 useEffect에서 ref값을 사용하라는 것이다.

 

// Inside of React
function useRef(initialValue) {
	const [ref, unused] = useState({ current: initialValue });
	return ref;
}
첫 번째 렌더링 중에 useRef 는 { current: initialValue }를 반환합니다. 이 객체는 React에 의해 저장되므로 다음 렌더링 중에 동일한 객체가 반환됩니다. 이 예제에서 state setter가 어떻게 사용되지 않는지 주목하세요. useRef는 항상 동일한 객체를 반환해야 하기 때문에 불필요합니다!


useRef는 React 내에서 위와 같이 동작하는데, useRef에서 초기값을 입력받은 다음 함수 형태로 현재 스코프보다 상위 스코프에서 저장하기에 리렌더링이 일어나도 값이 바뀌지 않는것으로 일단은 이해했다. 이 부분은 조금 더 찾아봐야겠다.


3. ref로 DOM 조작하기

또한 Js에서 특정 DOM을 선택할 때 getElementById, querySelector를 사용했듯이 DOM 값에 접근하기 위해서 ref를 사용할 수 있다. 그럼 또 DOM은 뭐냐? Document Object Model의 약자로 문서 객체 모델이라는 의미인데, 자세한 설명은 추후에 React의 리렌더링을 설명할 때 하겠다.

이렇게 생겨서 DOM Tree라고도 부른다. 사실 별로 나무같지는 않다.

 

Anyway, 공식문서의 예제 코드를 참조해서 어떻게 ref로 Dom을 조작하는지 알아보자.

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

위의 코드에서는

 

1. useRef를 불러오고 이를 변수 inputRef에 할당한다.

2. DOM 노드를 가져올 태그(여기서는 input 태그)에 ref={inputRef}를 추가해서 inputRef.current가 해당 태그의 DOM 노드를 가리키도록 한다.

3. 마지막으로 handleClick 함수에서 inputRef.current에 현재 있는 값을 focus하도록 했다.

 

그리고 당연히 useRef를 설정하거나 handleClick 함수가 실행되어도 해도 리렌더링은 일어나지 않는다.

 

그렇다면 위처럼 input과 같은 빌트인 컴포넌트가 아니라 다른 컴포넌트의 DOM 노드를 직접적으로 조작하려면 어떻게 해야 할까?

function MyInput(props) {
  return <input {...props} />;
}

export default function MyForm() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

<MyInput ref={inputRef} /> 처럼 직접적으로 다른 컴포넌트의 DOM 노드에 접근하려고 하면 제대로 동작하지 않는다. 컴포넌트는 기본적으로 다른 컴포넌트에게 자신의 DOM 노드를 공개하지 않기 때문이다. 이 때 사용하는 것이 forwardRef인데, 자세한 건 다음 글에서 적겠다.


4. Dropdown 컴포넌트로 알아보기

저번 프로젝트에서 사용한 DropDown 코드가 마침 ref를 사용해서 작성되어 있었다. 내가 짠 코드가 아니라 다른 친구가 짠 코드라 뜯어보면 공부도 될거니까 한번 살펴보자. 전체 소스코드는 여기 들어가보면 있음.

// 생략

export default function Dropdown({
  placeholder,
  list,
  selectItem,
  setSelectItem,
}: DropdownProps) {
  const dropdownListRef = useRef<HTMLUListElement>(null);
  const dropdownRef = useRef<HTMLDivElement>(null);
  const [isOpen, setIsOpen] = useState(false);

  const handleClickListItem = (e: React.MouseEvent<HTMLUListElement>) => {
    const closestListItem = (e.target as HTMLUListElement).closest('li');
    if (!closestListItem) return;
    setIsOpen(false);
    const foundItem = list.find((cur) => cur.id === closestListItem.id);
    if (foundItem) {
      setSelectItem(foundItem);
    }
  };
  useEffect(() => {
    if (isOpen && dropdownListRef.current) {
      dropdownListRef.current.scrollTop = 0;
    }
	// 생략
    document.addEventListener('click', handleOutsideClose);
    return () => document.removeEventListener('click', handleOutsideClose);
  }, [isOpen]);

  return (
    <div className="relative w-36 cursor-pointer p2r">
      <div
       	// 생략
        onClick={() => setIsOpen((prev) => !prev)}
        aria-hidden="true"
      >
	 // 생략
      <ul
        ref={dropdownListRef}
		// 생략
        onClick={handleClickListItem}
        aria-hidden="true"
      >
        {list.map(({ id, value }) => (
          <li
            key={id}
            id={id}
            className="h-10 text-center flex items-center justify-center"
          >
            {value}
          </li>
	// 생략

 

1. 우선 useRef를 사용하여 DOM 요소에 접근하는데 사용될 변수를 선언했다.

  const dropdownListRef = useRef<HTMLUListElement>(null);

 

2. 그 다음 ul태그에 dropdownListRef.current를 아래와 같이 연결해 참조할 수 있게 하였고

<ul
  ref={dropdownListRef}
  // 생략

 

3. 이제 연결된 ref를 활용해 스크롤 위치를 조정할 수 있다.

if (isOpen && dropdownListRef.current) {
  dropdownListRef.current.scrollTop = 0;
}

 

위의 부분을 봤을 때는 적절하게 잘 사용한 것 같다. ref 관련 공식문서를 살펴보면서 계속해서 나오는 말이 있는데, ref를 탈출구로 사용하라는 말이다. 즉, 스크롤을 조정하거나, focus를 맞추는 등 "React 외부로 나가야 할 때"만 사용하고 이 외에는 사용을 지양해야한다.

 

원래는 바로 forwardRef까지 적으려 했지만 글이 길어져 다음 글에서 이어서 작성하겠다.

 

공식문서로 forwardRef와 useImperativeHandle 알아보기

1. forwardRef는? 이제 forwardRef로 들어가보자. forwardRef는 자식 컴포넌트가 부모 컴포넌트에 DOM 노드를 노출할 때 사용한다. 나 같은 경우는 Atomic 패턴을 이용해 컴포넌트를 나누면서 부모 컴포넌트

cksxkr5193.tistory.com


5. 래퍼런스