2oneweek.dev

08. useRef

    Tags

  • React
08. useRef thumbnail

useRef

리액트로 작업하다보면, 실제 DOM 요소에 접근해야 하는 경우가 필요하다. 예를 들면, DOM 요소를 포커싱하거나 DOM 요소의 크기나 스크롤 위치를 알고 싶은 경우가 있다. 이때 useRef를 이용하면 자식 요소에 직접 접근할 수 있다.

  • 자식요소는 DOM일 수도 있고, 리액트 컴포넌트일 수도 있다.




기초 사용법

  • ref 속성과 useRef 훅 사용
import React, { useRef, useEffect } from "react"; export default function App() { const inputRef = useRef(); useEffect(() => { inputRef.current.focus(); }, []); return ( <div> <input type="text" ref={inputRef} /> <button> 저장 </button> </div> ); }
  • <input type="text" ref={inputRef} /> 에서 ref 속성값에 useRef가 반환해준 것을 넣어준다.
  • inputRef.current.focus();에서 current는 실제 DOM 요소를 가리킬 수 있다.
    • 따라서 DOM의 focus 함수를 실행할 수 있다.
  • ref 속성은 DOM 요소 뿐만 아니라, 컴포넌트에도 적용할 수 있다.

리액트의 렌더링 결과가 실제 돔에 반영된 후에 DOM 요소에 접근이 가능하다. 따라서 DOM요소에 접근하는 코드가 useEffect 안에 위치하고 있다.





컴포넌트에서 ref 사용

import React, { useRef, useEffect } from "react"; export default function App() { const inputRef = useRef(); useEffect(() => { inputRef.current.focus(); }, []); return ( <div> <InputAndSave inputRef={inputRef} /> <Button ref={inputRef} /> </div> ); } function InputAndSave({ inputRef }) { return ( <div> <input type="text" ref={inputRef} /> <button>저장</button> </div> ); } //forwardRef 함수 사용 const Button = React.forwardRef(function ({ onClick }, ref) { return ( <button onClick={onClick} ref={ref}> 저장 </button> ); });
  • 별다른 처리를 하지 않는다면, 컴포넌트에는 ref 속성 값을 사용할 수 없다.
    • 컴포넌트에 ref를 입력하면, 리액트가 내부적으로 처리하기 때문이다.
    • 따라서 위의 코드에선 inputRef라는 props를 만들어 내려보내고 있다.
  • <Button ref={inputRef} />와 같이 직관적으로 ref 속성을 사용하기 위해 forwardRef라는 함수를 사용한다.
    • forwardRef 함수를 사용하면, 두번째 매개변수로 ref 속성값을 받을 수 있다.

InputAndSave 컴포넌트에 넘겨주는 ref의 이름은 inputRef가 적절하다. 해당 컴포넌트 아래에는 많은 요소들이 있기 때문에, 어떤 참조 값인지 명시해주기 위해 ref 이름을 inputRef로 사용하는 것이 적절하다.





ref 속성 값에 함수를 입력

import React, { useRef, useEffect } from "react"; export default function App() { const [text, setText] = useState(INITIAL_TEXT); const [showText, setShowText] = useState(true); return ( <div> {showText && ( <input type="text" ref={(ref) => ref && setText(INITIAL_TEXT)} value={text} onChange={(e) => setText(e.target.value)} /> )} <button onClick={() => setShowText(!showText)}>보이기 / 가리기</button> </div> ); } const INITIAL_TEXT = "초기화";
  • useRef를 사용하지 않고 ref 속성값에 함수를 입력할 수 있다.
    • (ref) => ref && setText(INITIAL_TEXT)
    • 이 함수는 해당하는 요소가 생성되거나 사라질 때 한 번씩 호출된다
    • 생성될 때는 해당하는 요소의 레퍼런스가 인자로 넘어온다.
    • 사라질 때는 null 값이 넘어온다.
  • 이 코드에서는 생성될 때, INITIAL_TEXT로 초기화를 하게 된다.
    • 그러나 텍스트를 입력할 때 마다, 리렌더링 되면서 (ref) => ref && setText(INITIAL_TEXT) 함수가 재생성된다. 재 생성되면서 초기화를 진행하기 때문에 텍스트의 변경을 적용하지 못한다.
    • 이 함수를 고정시키기 위해 useCallback훅을 사용할 수 있다.


useCallback으로 ref 함수 고정

import React, { useRef, useEffect } from "react"; export default function App() { const [text, setText] = useState(INITIAL_TEXT); const [showText, setShowText] = useState(true); const setInitialText = useCallback((ref) => ref && setText(INITIAL_TEXT), []); return ( <div> {showText && ( <input type="text" ref={setInitialText} value={text} onChange={(e) => setText(e.target.value)} /> )} <button onClick={() => setShowText(!showText)}>보이기 / 가리기</button> </div> ); } const INITIAL_TEXT = "초기화";
  • useCallback의 메모이제이션 기능 덕분에, 한 번 생성된 setInitialText 함수를 계속해서 재사용 한다.




여러개의 DOM 요소에 접근

export default function App() { const boxListRef = useRef({}); let rightEnd = 0; let maxId = ""; function onClick() { for (const box of BOX_LIST) { const ref = boxListRef.current[box.id]; if (ref) { const rect = ref.getBoundingClientRect(); if (rightEnd < rect.right) { rightEnd = rect.right; maxId = box.id; } } } alert(maxId); } return ( <div> {BOX_LIST.map((item) => { <div key={item.id} ref={(ref) => (bokxListRef.current[item.id] = ref)} style={{ width: item.width }} >{`box_${item.id}`}</div>; })} <button onClick={onclick}>오른쪽 돌출부</button> </div> ); } const BOX_LIST = [ { id: 1, width: 100 }, { id: 2, width: 200 }, { id: 3, width: 200 }, { id: 4, width: 200 }, ];
  • 동적인 BOX_LIST를 참조하기 위해 useRef를 사용하는 것이 애매하다.
  • 이럴 때는 ref에 함수를 입력하는 방법을 사용한다.
    • 지금까지는 useRef가 반환하는 ref 객체에 실제 돔 요소나 컴포넌트의 인스턴스를 참조하는 용도로 사용했다.
    • 이 ref객체에는 어떤 값이나 저장할 수 있다는 장점이 있다.
      • ref={(ref) => (bokxListRef.current[item.id] = ref)} boxlist의 모든 요소의 레퍼런스를 boxListRef에 저장했다.




Ref 속성값 사용 시 주의할 점

컴포넌트가 생성된 이후라고 하더라도 ref객체의 CURRENT 속성이 없을 수 있다. 특히 조건부 렌더링의 경우 Ref 사용에 주의해야 한다.

export default function App() { const inputRef = useRef(); const [showText, setShowText] = useState(true); return ( <div> {showText && <input type="text" ref={inputRef} />} <button onClick={() => setShowText(!showText)}> 텍스트 보이기/가리기 </button> <button onClick={() => inputRef.current.focus()}>텍스트로 이동</button> </div> ); }

<button onClick={() => inputRef.current.focus()}>텍스트로 이동</button> 에서 ref 객체를 사용하고 있다.

  • 조건부 렌더링이 사용된 요소의 ref 객체를 사용할 수 있기 때문에, 조건부 렌더링을 참조할 땐 current 속성을 검사하는 코드가 필요하다.
    • 간단하게 옵셔널 체이닝을 사용하자 inputRef.current?.focus()




Example

첫 번째 예시

리-렌더에 영향을 끼치지도, 받지도 않는 상태를 저장하기 위해 useRef 사용

import React, { useRef, useEffect } from "react"; export default function App() { const timerIdRef = useRef(-1); useEffect(() => { timerIdRef.current = setTimeout(() => {}, 1000); }); useEffect(() => { if (timerIdRef.current >= 0) { clearTimeOut(timerIdRef.current); } }); }

Ref 객체는 꼭 참조 값만 담아야 하는 것은 아니다. 여기서는 타이머의 아이디를 담는다. 이렇게 렌더링과 상관 없는 값을 저장할 때 useRef가 유용하게 사용될 수 있다.

useState로도 저장할 수 있겠지만, 적합하지 않다. timerId가 변경될 때 다시 렌더링이 될 것이다.(timerId는 UI데이터가 아니기 때문에 렌더링 결과는 똑같다. 따라서 timerId 변화로 인한 리-렌더는 불필요한 렌더링이다.)




두 번째 예시

useEffect와 useRef를 이용하여 prev상태 보존

import React, { useState, useRef, useEffect } from "react"; export default function App() { const [age, setAge] = useState(20); const prevAgeRef = useRef(20); useEffect(() => { prevAgeRef.current = age; }, [age]); const prevAge = prevAgeRef.current; const text = age === prevAge ? "same" : age > prevAge ? "older" : "younger"; return ( <div> <p>{`age ${age} is ${text} than ${prevAge}`}</p> <button onClick={() => { const age = Math.floor(Math.random() * 50 + 1); setAge(age); }} > 나이 변경 </button> </div> ); }
  • 컴포넌트의 상태가 바뀌면, 컴포넌트 함수App()이 다시 실행된다. 따라서 모든 변수가 초기화된다.

    • 다시 렌더링 되어도 기존에 참조하고 있던 변수들을 유지하기 위해 useRef함수를 사용한다는 것을 잊지말자.
    • useRef가 반환하는 객체의 current 프로퍼티에 담긴 것은, 상태가 바껴도 컴포넌트가 리-렌더 되지 않으며 또한, 리-렌더가 발생해도 current의 값은 유지된다useRef(20)은 딱 한 번만 실행된다는 것이다.
  • useEffect는 렌더링이 끝난 후에 호출이 된다.

    • useEffect의 실행자 함수에서 current를 현재 나이로 변경한다.
    • current가 변경되어도 컴포넌트는 리-렌더 되지 않는다.
    • 따라서 예전의 값을 보존할 수 있는 것
Written by@2-one-week
현재 블로그 개발 중

GitHubLinkedIn