React 스터디 6주차 - useRef 란?

Yunes·2023년 9월 18일
0

리액트스터디

목록 보기
11/18
post-thumbnail

Ref 로 값을 참조하기

컴포넌트가 어떤 정보를 기억하길 원하나 그 정보가 새로운 렌더를 유발하는것을 원하지 않는다면 ref 를 사용할 수 있다.

import { useRef } from 'react';

const ref = useRef(0);

이때의 ref 는 다음과 같다.

{
	current: 0
}

따라서 현재의 값은 ref.current 프로퍼티를 통해 접근할 수 있다. 이 값은 읽거나 수정할 수 있는데 React 는 이 ref 를 추적하지 않는다. 이게 React 가 단방향 데이터 흐름의 탈출구를 만드는 이유이다.

위의 코드를 실행하면 ref.current 가 매 클릭 때마다 증가한다.

ref 와 state 의 공통점과 차이점

ref 는 state 처럼 숫자, 문자열, 객체, 심지어 함수도 될 수 있다.
단, state 와 달리 plain JavaScript opject 라서 refcurrent 프로퍼티 를 읽거나 수정할 수 있다.

ref 가 바뀔때는 컴포넌트가 재렌더링되지 않는다. 물론 재렌더링 될때마다 ref 는 React 에 의해 얻어진다. 단, state 를 변경하면 재렌더링되는 것과 달리 ref 가 바뀌어도 컴포넌트가 재렌더링되지 않는다.

ref 가 state 보다 덜 엄격해 보인다. state 와 달리 ref 는 직접 값을 수정해줄수도 있다. 그런데 보통 state 를 사용하며 ref 는 탈출용 해치로 그렇게 자주 사용하지는 않는다.

refsstate
useRef(initialValue) returns { current: initialValue }useState(initialValue) returns the current value of a state variable and a state setter function ( [value, setValue])
Doesn’t trigger re-render when you change it.Triggers re-render when you change it.
Mutable—you can modify and update current’s value outside of the rendering process.“Immutable”—you must use the state setting function to modify state variables to queue a re-render.
You shouldn’t read (or write) the current value during rendering.You can read state at any time. However, each render has its own snapshot of state which does not change.

counter 로 보는 ref 와 state 비교

state

ref

위의 경우처럼 ref.current 는 각 렌더링에서 읽어와도 믿을수 없는 코드가 될 수 있으니 필요하다면 state 를 사용하자.

stopwatch 만들기

state 와 ref 를 같이 사용해서 스탑워치를 만들어보자.

스탑워치는 시작과 종료기능이 있다. 이때 시간이 다르게 나타나도록 하려면 렌더링때 해당 정보가 필요하다는 의미이니 state 로 생성해야 한다.

const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);

만약 종료 버튼을 클릭했다면 setInterval 을 지워주기 위해 clearInterval을 사용할 수 있는데 종료 버튼을 클릭할 때 렌더링이 될 필요는 없으니 ref 는 현재까지 흐른 시간정보를 갖고 있다.

재렌더링때 필요한 정보면 state 를 사용하자.
만약 정보가 이벤트 핸들러에 의해서만 사용되고 그 값이 바뀌는 것이 재렌더링을 필요로 하지 않으면 ref 를 사용하자.

useRef 가 내부적으로 어떻게 동작할까?

useRef 는 setState 가 없는 useState 처럼 동작한다.

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

useRef 는 setState 가 없다. 그래서 첫 렌더링때 초기값을 ( { current: initialValue } ) 반환하고 그 다음 렌더링때에도 같은 객체를 반환한다. ( 항상 같은 객체를 반환한다. )

그럼 ref 는 언제 사용할까?

Ref 를 사용하는 경우

컴포넌트가 React 바깥에서 외부 API 와 통신하는 경우 ( 컴포넌트 외관에 영향을 끼치지 않는 브라우저 API 등 ) ref 를 일반적으로 사용한다.

예시 )

  • timout ID 저장
  • 다음 페이지에서 다루는 DOM element 저장 및 조작
  • JSX 를 계산하는데 필수적이지 않은 객체를 저장

만약 컴포넌트가 어떤 값을 저장해야 하는데 렌더링 로직에 영향을 끼치지 않는다면 ref 를 사용하자.

ref 의 좋은 사용사례

  • ref 를 escape hatch 로 취급하자.
    ref 는 외부 시스템이나 브라우저 API 를 사용하고자 할때 유용하다. 단 너무 많은 애플리케이션 로직이나 데이터흐름이 ref 에 의존중이라면 ref 를 사용하는 것을 다시 생각해 볼 필요가 있다.

  • ref.current 를 렌더링 도중에 읽거나 쓰지 말자.
    만약 렌더링 도중 어떤 정보가 필요하다면 ref 대신 state 를 사용하자. 렌더링 도중 ref.current 가 변하는지 여부를 React 가 알기 어려우니 컴포넌트의 행동을 예측하기 어렵게 만든다.

단, state 가 매 렌더링마다 스냅샷처럼 행동하는 것과 동기적으로 업데이트하지 않는다는 것과 같은 제약이 ref 에 적용되지 않는다. 그래서 ref 의 현재 값은 즉각적으로 바꿀 수 있다.

ref.current = 5;
console.log(ref.current); // 5

또한 ref 를 사용중일때 변동을 걱정할 필요가 없다. 해당 객체를 바꾸는 중에 이는 렌더링에 사용되지 않는다. React 는 ref 와 그 내부요소에 무엇을 하든 관심을 갖지 않는다.

escape hatch

  • 특정한 상황에서 시스템이나 라이브러리의 제한된 기능을 벗어나거나 우회하는 방법을 나타내는 용어

Ref 와 DOM

ref 에 그 어떤 값도 올수 있으나 일반적으로는 DOM 요소에 접근하기 위해 ref 를 사용한다. 예를 들어 input 에 focus 를 하고 싶을때 사용한다. ref 를 JSX 에서 ref 어트리뷰트에 전달하면 ( <div ref={myRef}> ) React 는 myRef.current 에 상응하는 DOM 요소를 넣는다.

Ref 로 DOM 조작하기

React 는 렌더 결과에 적합하게 DOM 을 자동으로 업데이트하기에 컴포넌트는 DOM 을 자주 조작할 필요가 없다. 그러나 노드에 focus 하거나 스크롤 할때, 혹은 DOM 의 크기와 위치를 측정해야 할때처럼 DOM 요소에 접근해야 할 때가 있다. React 의 내장 방식중에 이렇게 할 방법이 없기에 DOM 노드로의 ref 가 필요하다.

노드로의 ref 를 가져오기

import { useRef } from 'react';

const myRef = useRef(null);
<div ref={myRef}/>

// You can use any browser APIs, for example:
myRef.current.scrollIntoView();

예시 : text input 에 focus

이 경우 버튼을 클릭하면 input 에 focus 를 갖는다.

DOM 조작이 ref 의 가장 흔한 사용사례이나 useReftimer ID 같이 React 외부의 값을 저장하는데에도 사용된다.

예시 : 요소 스크롤

컴포넌트 내에서 하나 이상의 ref 를 사용할 수 있다.

위의 예시 코드에선 일치하는 DOM node 에 scrollIntoView() 메서드를 호출하여 이미지를 중앙에 위치하도록 한다.

mdn - element.scrollIntoView

element.scrollIntoView();
element.scrollIntoView(alignToTop); // Boolean parameter
element.scrollIntoView(scrollIntoViewOptions); // Object parameter

alignToTop : 불리언 값

  • true 일때 요소의 상단이 스크롤 가능한 조상 요소의 보이는 영역 상단에 정렬된다.

    • scrollIntoViewOptions: {block: "start", inline: "nearest"} 와 일치하며 기본값이다.
  • false 일때 요소의 하단이 스크롤 가능한 조상 요소의 보이는 영역 하단에 정렬된다.

    • scrollIntoViewOptions: {block: "end", inline: "nearest"} 와 일치

scrollIntoViewOptions

  • behavior : 스크롤 즉시 적용할지 아니면 부드러운 애니메이션 적용할지 결정

    • smooth : 스크롤이 부드럽게 움직인다.
    • instant : 스크롤이 즉시 적용된다.
    • auto : 스크롤 동작은 scroll-behavior 의 계산된 값에 의해 결정된다.
  • block : 수직 정렬

    • start / center / end / nearest
    • 기본값은 start
  • inline : 수평 정렬

    • start / center / end / nearest
    • 기본값은 nearest

예시 코드

const element = document.getElementById("box");

element.scrollIntoView();
element.scrollIntoView(false);
element.scrollIntoView({ block: "end" });
element.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });

다른 컴포넌트 DOM 노드에 접근하기

<input /> 과 같은 브라우저 요소를 반환하는 내장 컴포넌트에 ref 를 전달하면 React 는 ref 의 current 속성을 상응하는 DOM 노드로 설정한다.

그러나 내가 자체적으로 만든 <MyInput /> 과 같은 컴포넌트에 ref 를 전달하면 기본적으로 null 을 갖는데 다음 코드를 보면 그 이유를 알 수 있다. 버튼을 클릭하면 input 에 focus 를 갖지 않고 오류를 반환한다.

동작하지 않는 코드

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

이런 오류가 뜨는 까닭은 React 가 이들의 자식 노드까지 포함하여 다른 컴포넌트의 DOM 노드에 접근하는 것을 기본적으로 허락하지 않기 때문이다.

이는 의도적인 것인데 Ref 자체가 escape hatch 로 드물게 사용해야 하는 것인데 다른 컴포넌트의 DOM 노드를 조작할 수 있다면 이는 코드를 더욱 취약하게 만들 것이다.

대신 컴포넌트가 그들의 DOM 노드를 노출하길 원한다면 동작을 선택해야 한다. 컴포넌트는 ref 를 하위 항목중 하나로 전달하도록 지정할 수 있다.

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

위의 코드는 다음과 같은 순서로 작동한다.

  1. <MyInput ref={inputRef} /> 는 React 에게 상응하는 DOM 노드를 inputRef.current 에 전달하라고 말하나 이 동작은 MyInput 컴포넌트에 달려있고 기본적으로 그렇게 되지 않는다.

  2. MyInput 컴포넌트가 forwardRef 를 사용해서 선언되었다. 이는 props 이후 선언된 두번째 인자인 ref 를 통해 inputRef 를 받을 수 있게 한다.

  3. MyInput 컴포넌트는 자체적으로 받은 ref 를 내부의 input 으로 전달한다.

동작하는 코드

디자인 시스템에서 button, input 등과 같은 low-level 컴포넌트는 ref 를 DOM 노드로 전달하는 것이 일반적이나 forms, lists, page section 과 같은 high-level 컴포넌트는 DOM 구저에 갑작스런 의존성을 피하기 위해 DOM 노드에 노출하지 않는 것이 일반적이다.

React 가 Ref 를 첨부할 때

React 는 모든 업데이트를 2 단계로 쪼갠다.

  1. 매 렌더링때 React 는 화면에 무엇을 나타내야 할지 계산하기 위해 컴포넌트를 호출한다.
  2. 매 커밋때 React 는 DOM 에 변경점을 적용한다.

일반적으로 렌더링때 ref 에 접근하기 원하지 않을 것이다.

일반적으로 첫 렌더링때 DOM 노드는 아직 생성되지 않았으니 ref.currentnull 이 된다. 또한 변경점을 렌더링하는 동안 DOM 은 아직 업데이트되지 않았으니 ref 를 읽기에 너무 이른 시점이다.

React 는 커밋하는 동안 ref.current 를 설정한다. DOM 을 업데이트하기 전에 React 는 영향받은 ref.current 값을 null 로 설정한다. DOM 을 업데이트한 이후엔 React 는 상응하는 DOM 노드로 ref.current 를 설정한다.

일반적으로 event handler 에서 ref 에 접근할 것이다. 만약 ref 를 사용하고 싶은데 특정한 event 가 없다면 Effect 를 사용하는 것이 더 나을 수 있다.

ref 를 사용한 DOM 조작 모범 사례

Ref 는 escape hatch 이다. React 에서 벗어나 있을때 사용해야 한다. 일반적인 사용 사례는 focus, scroll position, React 가 노출하지 않는 brower API 호출 등을 다루는 경우이다.

만약 focus, scrolling 같은 비파괴 행동을 한다면 문제를 접하지 않을 것이나 만약 DOM 을 의도적으로 수정하려 한다면 React 가 만드는 변화와 충돌이 발생할 위험이 있다.

위의 코드는 안내 메세지와 2개의 버튼이 있다.

첫번째 버튼은 React 에서 보통 동작하는 조건부 렌더링과 state 를 사용하여 메세지를 토글한다.

두번째 버튼은 React 제어의 바깥에서 DOM 을 강제로 제거하는 DOM API remove() 를 사용한다.

첫번째 버튼은 여러번 클릭하면 메세지가 나타났다가 다시 등장한다.
두번째 버튼을 클릭하면 메세지가 사라진다.

그 이후 첫번째 버튼을 클릭하면 충돌이 발생하는데 이는 DOM 을 변경시켰으니 React 는 어떻게 올바르게 제어해야 할지 알지 못하기 때문에 발생한 것이다.

React 가 관리하는 DOM 노드를 변경하지 말자. React 에 의해 관리되는 요소노드를 수정하거나 자식노드를 추가, 삭제하면 일관적이지 않는 시각적 결과나 충돌이 발생할 수 있다.

그러나 DOM 제어를 전적으로 사용하면 안된다는 뜻이 아니다. 주의가 필요하다는 의미이다. React 가 업데이트할 이유가 없는 DOM 은 안전하게 수정할 수 있다. 예를 들어 어떤 div 태그가 JSX 에서 항상 비어있다면 React 는 리스트에서 자식 노드를 건드릴 이유가 없다. 그러니 이런 경우 요소를 추가하거나 삭제하는 것은 안전하다.

useRef

const ref = useRef(initialValue)

useRef 는 렌더링때 필요하지 않는 값을 참조할 수 있도록 하는 React 훅이다.

initialValue : ref 객체의 current 프로퍼티를 초기화하기 위한 값이다. 어떤 타입이든 사용할 수 있다. 첫 렌더링 이후 이 인자는 무시된다.

useRef 는 current 라는 하나의 프로퍼티를 갖는 객체를 반환한다.

current : 초기에 initialValue 를 통해 값이 정해진다. 이후에 다른 값으로 변경할 수 있다. 만약 React 에서 JSX 노드의 ref 어트리뷰트로 ref 객체를 전달한다면 React 는 current 프로퍼티로 설정한다.

다음 렌더링때 useRef 는 같은 객체를 반환한다.

state 와 달리 ref.current 는 수정할 수 있다. 그러나 만약 렌더링때 사용되는 객체라면 수정해서는 안된다. 물론 이런 경우엔 state 를 사용해야 한다.

ref.current 프로퍼티를 바꿀때 React 는 재렌더링 되지 않는데 ref 가 plain JavaScript object 이기 때문에 React 는 ref 가 바뀌는 것을 알 수 없다.

초기화를 제외하고 ref.current 를 렌더링하는 동안 읽거나 쓰지 말자. 이는 컴포넌트의 동작을 예측불가능하게 만든다.

StrictMode 에서 우발적인 불순물을 찾기위해 개발환경에서 컴포넌트 함수를 두번 호출한다. 이때 ref 객체는 두번 생성되나 하나의 버전은 삭제된다. 만약 컴포넌트 함수가 순수함수라면 ( 그래야 하기도 한다. ) 이는 동작에 영향을 끼치면 안된다.

ref.current 를 렌더링 동안 읽거나 쓰지 말자

React 는 컴포넌트가 순수함수처럼 동작한다고 예측한다. 만약 props, state, context 같은 입력이 같다면 같은 JSX 를 반환해야 한다.

이들을 다른 순서나 다른 인자와 함께 호출하는 것은 다른 호출에 영향을 끼쳐서는 안된다.

렌더링 도중 ref 를 읽거나 쓰면 안된다.

function MyComponent() {
  // ...
  // 🚩 Don't write a ref during rendering
  myRef.current = 123;
  // ...
  // 🚩 Don't read a ref during rendering
  return <h1>{myOtherRef.current}</h1>;
}

이벤트 핸들러나 ㄷffect 에서 ref 를 읽거나 쓰자

function MyComponent() {
  // ...
  useEffect(() => {
    // ✅ You can read or write refs in effects
    myRef.current = 123;
  });
  // ...
  function handleClick() {
    // ✅ You can read or write refs in event handlers
    doSomething(myOtherRef.current);
  }
  // ...
}

렌더링 동안 읽거나 써야 한다면 state 를 대신 사용하자.

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글