본문 바로가기

나야, 리액트 스터디

[Week 6] Ref

 

 

🍥 Ref란?

ref는 React 컴포넌트에서 DOM 요소 또는 React 컴포넌트 인스턴스에 직접 접근할 수 있는 방법을 제공한다.

일반적으로 React는 데이터의 흐름을 통해 UI를 관리하지만, ref는 예외적으로 직접적인 접근이 필요할 때 사용된다.

 

  • React가 렌더링 사이클에서 관리하지 않는 값(예: DOM 요소)과의 상호작용에 사용
  • 렌더링에 영향을 주지 않으며 값이 변해도 컴포넌트를 다시 렌더링하지 않음

 

 

1️⃣ Ref 사용법

 

✔️ useRef를 사용해서 ref를 생성

import { useRef } from 'react';

const ref = useRef(0);

 

 

✔️ useRef가 반환하는 객체 : current

ref.current 프로퍼티를 통해 해당 ref의 current 값에 접근할 수 있고, 읽고 쓰기가 가능하다 !

{
	current: 0 // useRef에 전달한 값
}

 

 

 

2️⃣ useRef 동작 과정

function useRef(initialValue) {
	const [ref, unused] = useState({ current: initialValue });
    return ref;
}

 

useRef는 항상 동일한 객체를 반환해야 하므로, set 함수가 필요 없다. 즉, setter가 없는 일반적인 state 변수

 

 

3️⃣ ref를 언제 사용해야 할까?

리액트를 외부와 외부 api 컴포넌트 형태에 영향을 미치지 않는 브라우저 api와 통신해야 할 때 사용한다.

  • timeout IDs를 저장
  • DOM 엘리먼트 저장 및 조작
  • JSX를 계산하는데 필요하지 않은 다른 객체 저장

 


🍥 State와 Ref의 차이

Ref는 state처럼 문자열, 객체, 함수 등 모든 것을 가리킬 수 있지만,

state와 달리 읽고 수정할 수 있는 current 프로퍼티를 가진 일반 자바스크립트 객체이다.


State Ref
값이 바뀌면 컴포넌트를 재렌더링함 값이 바뀌어도 컴포넌트를 재렌더링하지 않음
React가 값의 변경을 추적함 React는 값의 변경을 추적하지 않음
주로 UI 업데이트를 위한 데이터 관리 주로 DOM 접근 또는 렌더링 외부 값을 저장
언제든지 state를 읽을 수 있음
(각 렌더마다 변경되지 않는 자체 스냅샷이 있음)
렌더링 중에 current 값을 읽거나 쓰면 안됨
외부에서 current 값을 수정 및 업데이트 가능 state 설정 함수를 사용해 리렌더 대기열에 넣어야 함

 

리액트는 렌더링에 정보를 사용할 때 해당 정보를 state로 유지하는데,

이벤트 핸들러에게만 필요한 정보이고 변경이 일어날 때 리렌더링이 필요하지 않다면, ref를 사용하는 것이 더 효율적이다!

 

 

 

🍥 DOM 요소 접근

ref를 사용하면 특정 DOM 요소에 접근하여 속성을 변경하거나 포커스를 설정할 수 있다.

특정 노드에 포커스를 옮기거나, 스크롤 위치를 옮기거나, 위치와 크기를 측정하는 등

  

 

1️⃣ 입력창에 포커스를 설정하는 예시

 

import { useRef } from 'react';

function InputFocus() { 
	const inputRef = useRef(null); 
    const handleFocus = () => { 
    	inputRef.current.focus(); // DOM 요소에 직접 접근하여 포커스 설정 
     }; 
     
     return ( 
     	<div> 
        	<input ref={inputRef} type="text" placeholder="클릭하면 포커스됨" /> 
            <button onClick={handleFocus}>포커스 설정</button> 
        </div> 
     ); 
 }
 
 

초기에는 ref.current가 null이다가,

리액트가 input에 대한 DOM 노드를 생성할 때, 리액트는 이 노드에 대한 참조를 ref.current에 넣는다.

DOM 노드를 이벤트 핸들러에서 접근하거나, 노드에 정의된 내장 브라우저 API를 사용할 수 있다.

 
 

 

2️⃣ 한 요소로 스크롤을 이동하는 ref 사용 예시 

 

import { useRef } from 'react';

export default function CatFriends() {
  const firstCatRef = useRef(null);
  const secondCatRef = useRef(null);
  const thirdCatRef = useRef(null);

  function handleScrollToFirstCat() {
    firstCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function handleScrollToSecondCat() {
    secondCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function handleScrollToThirdCat() {
    thirdCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  return (
    <>
      <nav>
        <button onClick={handleScrollToFirstCat}>
          Neo
        </button>
        <button onClick={handleScrollToSecondCat}>
          Millie
        </button>
        <button onClick={handleScrollToThirdCat}>
          Bella
        </button>
      </nav>
      <div>
        <ul>
          <li>
            <img
              src="https://placecats.com/neo/300/200"
              alt="Neo"
              ref={firstCatRef}
            />
          </li>
          <li>
            <img
              src="https://placecats.com/millie/200/200"
              alt="Millie"
              ref={secondCatRef}
            />
          </li>
          <li>
            <img
              src="https://placecats.com/bella/199/200"
              alt="Bella"
              ref={thirdCatRef}
            />
          </li>
        </ul>
      </div>
    </>
  );
}

 

 

🍀 ref 리스트 관리하기 [ref 콜백]

목록의 아이템마다 ref가 사용될 수도 있고, ref가 얼만큼 필요할지 예측할 수 없는 경우가 있다.
이 경우에는, 부모 요소에서 단일 ref를 얻고, querySelectAll과 같은 메서드를 사용해 개별 자식 노드를 찾는 방식도 있지만,
ref 어트리뷰트를 함수에 전달하는 방법으로 조작하는 방식이 더 좋다. 

 

import { useRef, useState } from "react";

export default function CatFriends() {
  const itemsRef = useRef(null);
  const [catList, setCatList] = useState(setupCatList);

  function scrollToCat(cat) {
    const map = getMap();
    const node = map.get(cat);
    node.scrollIntoView({
      behavior: "smooth",
      block: "nearest",
      inline: "center",
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // 처음 사용하는 경우, Map을 초기화합니다.
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToCat(catList[0])}>Neo</button>
        <button onClick={() => scrollToCat(catList[5])}>Millie</button>
        <button onClick={() => scrollToCat(catList[9])}>Bella</button>
      </nav>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat, node);
                } else {
                  map.delete(cat);
                }
              }}
            >
              <img src={cat} />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

function setupCatList() {
  const catList = [];
  for (let i = 0; i < 10; i++) {
    catList.push("https://loremflickr.com/320/240/cat?lock=" + i);
  }

  return catList;
}

 

ref를 설정할 때 DOM 노드와 함께 ref 콜백을 호출하며, ref를 지울 때에는 null을 전달한다.

 

 

 

🍀 직접 만든 컴포넌트에 ref 주입하기 [forwardRef]

외부에서 주입하는 ref를 컴포넌트 내부의 React Element에 전달하는 역할을 한다.
컴포넌트 사용 시 두번째 파라미터로 외부에서 설정해준 ref를 참조할 수 있게 된다.

 

import { useRef } from 'react';

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>
    </>
  );
}

 

직접 만든 컴포넌트에 ref를 주입할 때는 null이 기본적으로 주어지기 때문에,
아래 코드에선 버튼을 클릭할 경우, MyInput에 접근할 수 없다는 런타임 에러가 발생한다.

 

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

 

따라서, 자식 중 하나에 ref를 전달하도록 지정해서, 특정 컴포넌트에서 소유한 DOM 노드를 선택적으로 노출한다.

 

 

🍀 ref 커스터마이징하기 [useImperativeHandle]

forwardRef를 사용해서 컴포넌트에 ref를 전달할 수 있게 되었지만 여전히 ref를 통해서 제어할 수 있는 것은 DOM 객체 뿐useImperativeHandle을 사용하면 ref에 할당되는 값을 DOM 객체가 아닌, 컴포넌트 내부에서 커스터마이징한 객체로 변경 가능하다.

즉, 자식 컴포넌트에서 노출하고 싶은 ref 객체를 따로 정의할 수 있다는 것이다.

 

function ChildComponent(props, ref) {
  useImperativeHandle(ref, () => {
    return {
      getText: () => 'useImperativeHandle 테스트'
    };
  }, []);

  return <span>children ref 테스트</span>
}

const ForwardedChild = forwardRef(ChildComponent);

function ParentComponent() {
  const childRef = useRef(null);
  
  useEffect(() => {
    console.log(childRef.current?.getText()); // 'useImperativeHandle 테스트'
  }, []);

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

 

 

🍥 Ref 사용 시 주의사항

필요한 경우에만 사용하기

React는 기본적으로 상태를 통해 UI를 관리하도록 설계되었기 때문에, ref는 렌더링 사이클로 해결할 수 없는 문제에만 사용하기

 

렌더링 최적화 주의

ref는 값이 변경되어도 렌더링에 영향을 주지 않으므로, 상태 관리가 필요한 경우에는 적절히 state를 사용하기

 

제어 컴포넌트와 비제어 컴포넌트 구분

ref는 비제어 컴포넌트와 함께 사용할 때 적합하므로, 제어 컴포넌트에서는 상태를 통해 데이터를 관리하기

 

 


 

 
 

ref가 DOM 조작을 위해 사용되는 개념이라는 정도는 알았지만

다양한 사용 예시를 통해 ref를 사용하는 방법에 대해선 이번에 새롭게 익히게 된 것 같다.

state를 통해 구현할 수 있는 것들이라 그동안은 크게 필요성을 느끼지 못해 공부하지 않았는데,

필요할 때 적절히 사용하면 불필요한 리렌더링을 줄일 수 있다는 이점이 있으니까 알아두는게 훠얼씬 좋은 것 같기도 하고 ..

 

이번주차 공부하면서 궁금했던 점을 꼽아보자면 !
공식문서를 통해 flushSync나 useImperativeHandle에 대해서 처음 알게되었는데, 아직 확 와닿지 않는 것 같아서,
이 친구들을 직접 사용했던 경험이 있다면 그 경험에 대해 듣고싶어요 !

 

+) 

이 궁금증을 가지고 찾아보다가 어렵지만 재미있는 경험이 있는 것 같아 들고왔습니다 😇

https://velog.io/@young_mason/useImperativeHandle 

 
 
 

 

'나야, 리액트 스터디' 카테고리의 다른 글

[week7] Effect& custom Hook  (4) 2024.12.08
[week7] useEffect, customHook  (2) 2024.12.08
[week6]탈출구 - Ref로 값 참조하기, Ref로 DOM조작하기  (2) 2024.12.01
[week 6] useRef 뿌시기  (3) 2024.12.01
[week6] Ref  (1) 2024.12.01