[React] useState 대신 useRef를 사용해야 하는 경우

주니어 개발자를 위한 React 실전 가이드 <3>
May 28, 2024
[React] useState 대신 useRef를 사용해야 하는 경우
 
 
🗣️
React 개발을 하다 보면 컴포넌트 내에서 DOM의 요소나 특정 값을 참조해야 하는 상황이 자주 발생하는데요. 이때 useRef Hook을 사용하면 렌더링과 무관하게 값을 유지할 수 있습니다. 5년 차 소프트웨어 엔지니어 현명 님이 주니어 개발자들이 알면 좋을 법한 useRef의 사용 원리와 사례를 실제 예제와 함께 설명해 주실 예정이니, useRef에 대해 제대로 이해하고 싶은 분들은 이번 아티클에 주목해 주세요.
 
 
React에서 useRef는 함수형 컴포넌트 내에서 DOM 요소나 다른 React 요소를 참조하기 위해 사용되는 Hook입니다. useRef를 사용하면 클래스 컴포넌트의 this.refs와 유사한 기능을 함수형 컴포넌트에서도 사용할 수 있습니다. useRef를 사용하면 컴포넌트의 렌더링과 관련 없이 동일한 참조를 유지할 수 있습니다. 이는 컴포넌트가 리렌더링되어도 참조가 변경되지 않고 이전 요소를 가리키도록 하는 데 유용합니다. 예를 들어, useRef를 사용하여 특정 DOM 요소에 대한 참조를 가져올 수 있습니다. 이렇게 하면 해당 DOM 요소에 접근하여 프로그래밍 방식으로 조작할 수 있습니다.
 
import React, { useRef, useEffect } from 'react'; const MyComponent = () => { const myRef = useRef(null); useEffect(() => { // 컴포넌트가 마운트되면 DOM 요소에 포커스를 설정합니다. myRef.current.focus(); }, []); return <input ref={myRef} />; }; export default MyComponent;
 
이 예제에서 useRef를 사용하여 myRef라는 변수를 생성하고 <input> 요소에 할당합니다. useEffect 훅을 사용하여 컴포넌트가 마운트될 때 해당 요소에 포커스를 설정합니다. 이때 myRef.current를 사용하여 실제 DOM 요소에 접근합니다. 그렇다면 어떨 때 useState 대신 useRef를 사용해야 할까요?
useState와 useRef는 React Hooks에서 각각 다른 목적으로 사용됩니다.
 
  • useState
    • 컴포넌트의 상태를 관리할 때 사용됩니다. 상태가 변할 때마다 컴포넌트를 다시 렌더링하고 싶을 때 주로 사용됩니다. 상태를 변경하면 해당 컴포넌트가 다시 렌더링됩니다.
  • useRef
    • 이전 값과 새로운 값을 연결하고, 렌더링과는 무관하게 값이 유지되어야 할 때 사용됩니다. 주로 DOM 요소나 외부 라이브러리의 인스턴스와 같이 렌더링과는 직접적으로 관련이 없는 값들을 저장할 때 사용됩니다.
 
따라서 useState와 useRef는 서로 다른 용도를 가지고 있으며, 각각의 사용 사례에 따라 선택되어야 합니다.
useRef를 사용하는 일반적인 상황은 다음과 같습니다.
 
  • 컴포넌트의 렌더링과 관련이 없는 값들을 저장할 때
  • DOM 요소에 대한 참조를 저장할 때
  • 렌더링 과정에서 값이 변경되어도 다시 렌더링되지 않아도 되는 경우
 
 
 

useState 예제

좀 더 이해하기 쉽게 예제와 함께 살펴보겠습니다.
 
import React, { useState } from 'react'; const Counter = () => { const [count, setCount] = useState(0); const increment = () => { setCount(count + 1); }; return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); }; export default Counter;
 
이 예제에서는 useState 훅을 사용하여 count 상태와 이를 업데이트하는 함수 setCount를 정의합니다. count 상태는 컴포넌트의 렌더링에 직접적으로 영향을 주므로, 상태가 변경될 때마다 컴포넌트가 다시 렌더링됩니다.
 
 
 

useRef 예제

import React, { useRef, useEffect } from 'react'; const Timer = () => { const intervalRef = useRef(null); const secondsRef = useRef(0); useEffect(() => { intervalRef.current = setInterval(() => { secondsRef.current += 1; console.log('Seconds:', secondsRef.current); }, 1000); return () => { clearInterval(intervalRef.current); }; }, []); return ( <div> <p>Timer: {secondsRef.current} seconds</p> <button onClick={() => clearInterval(intervalRef.current)}>Stop</button> </div> ); }; export default Timer;
 
이 예제에서는 useRef 훅을 사용하여 intervalRef와 secondsRef라는 두 개의 참조를 생성합니다. intervalRef는 setInterval의 반환값을 저장하고, secondsRef는 시간을 추적하는 데 사용됩니다. useEffect 훅을 사용하여 컴포넌트가 마운트될 때 setInterval을 시작하고, 언마운트될 때 정리합니다. secondsRef는 컴포넌트의 렌더링과 무관하게 값이 유지됩니다. 또한 secondsRef.current를 통해 현재 초를 추적하고 화면에 표시합니다.
 
이 두 예제를 통해 useState는 컴포넌트의 상태를 관리하고 렌더링을 유발하는 데 사용되는 반면, useRef는 렌더링과 관련이 없는 값들을 저장하거나 DOM 요소에 접근하는 데 사용된다는 것을 알 수 있습니다. 데이터가 많을수록 useRef를 사용해야 하는 경우가 많은데, 이번에는 같은 프로그램의 예제를 통해 알아보겠습니다.
 
 
 

useState를 사용한 예제

import React, { useState } from 'react'; const BigDataComponent = () => { const [data, setData] = useState(''); const fetchData = () => { // 대용량 데이터를 가져온다고 가정 // 이 데이터가 많을수록 렌더링이 지연될 수 있음 const newData = 'Very large data...'; setData(newData); }; return ( <div> <button onClick={fetchData}>Fetch Data</button> <p>Data Length: {data.length}</p> </div> ); }; export default BigDataComponent;
이 예제에서 useState를 사용하여 데이터를 관리하고, 버튼을 클릭할 때마다 대용량 데이터를 가져오는 fetchData 함수를 호출합니다. 그러나 데이터 양이 많아질수록 이 작업은 렌더링 속도를 늦출 수 있습니다.
 
 
 

useRef를 사용한 예제

import React, { useRef } from 'react'; const BigDataComponent = () => { const dataRef = useRef(''); const fetchData = () => { // 대용량 데이터를 가져온다고 가정 // useRef를 사용하여 컴포넌트의 렌더링에 영향을 주지 않고 데이터를 설정 const newData = 'Very large data...'; dataRef.current = newData; }; return ( <div> <button onClick={fetchData}>Fetch Data</button> <p>Data Length: {dataRef.current.length}</p> </div> ); }; export default BigDataComponent;
 
이 예제에서는 useRef를 사용하여 데이터를 관리합니다. 데이터 양이 증가하더라도 useRef를 사용하면 컴포넌트의 렌더링 속도에 영향을 주지 않고 데이터를 설정할 수 있습니다. 이 두 예제를 통해 데이터 양이 증가함에 따라 useState가 느리게 동작하는 반면, useRef를 사용하면 데이터 양이 증가해도 렌더링에 영향을 주지 않고 데이터를 관리할 수 있음을 알 수 있습니다. useState와 useRef의 차이점과 데이터 양에 따른 동작 차이의 특징을 정리해보겠습니다.
 
  • 렌더링과 무관한 값 보존
    •  useRef로 생성한 ref 객체의 current 속성에 할당된 값은 컴포넌트가 재렌더링되어도 변경되지 않습니다. 이는 useRef를 사용하여 컴포넌트의 렌더링과 관계없이 값이 유지되도록 할 수 있는데, 이는 일반적으로 컴포넌트의 상태가 아닌 값을 보존하는 데 유용합니다.
       
  • 렌더링 사이클 동안 값이 변경되어도 렌더링을 트리거하지 않음
    •  useRef로 생성된 객체의 current 속성에 값을 할당하더라도 컴포넌트는 다시 렌더링되지 않습니다. 이는 useState와 다르게 상태 업데이트가 렌더링을 트리거하지 않고 값을 변경할 수 있다는 것을 의미합니다. 이러한 특성은 DOM 요소의 참조, 외부 라이브러리의 인스턴스 등과 같이 렌더링과 무관한 값들을 관리할 때 유용합니다.
       
이러한 특성들은 useRef를 사용하여 컴포넌트의 렌더링과는 별개로 데이터를 관리하거나 DOM 요소에 접근하는 등의 작업을 수행할 때 매우 유용합니다. 이를 잘 사용하기 위해서는 리액트의 렌더링에 대한 이해가 필수적으로 수반되어야 합니다.
 
 
 

리엑트 렌더링

아래는 기본 개념부터 최적화까지 렌더링에 대해 간단히 정리한 부분입니다. 리액트의 렌더링은 컴포넌트의 상태나 속성(props)이 변경되었을 때 화면에 어떻게 반영되는지에 대한 과정을 의미합니다. 이를 이해하기 위해서는 리액트의 가상 돔(Virtual DOM) 개념부터 시작하는 것이 좋습니다.
 

1. 가상 돔(Virtual DOM)

가상 돔은 리액트가 실제 DOM을 추상화한 가상의 돔 구조입니다. 리액트는 상태 변화 등의 이벤트가 발생하면 가상 돔을 업데이트하고, 실제 DOM과 비교하여 변경 사항을 찾아내고 최소한의 연산만 수행하여 실제 DOM을 업데이트합니다. 이 과정을 통해 성능을 향상시키고 불필요한 렌더링을 방지합니다.
 

2. 컴포넌트 렌더링 과정

  • 초기 렌더링
    • 리액트 애플리케이션이 시작될 때, 컴포넌트 트리가 렌더링되어 초기 UI가 생성됩니다.
  • 상태나 속성 변경
    • 사용자 상호작용 등의 이벤트에 의해 상태(state)나 속성(props)이 변경될 수 있습니다.
  • 가상 돔 업데이트
    • 상태나 속성이 변경되면 리액트는 해당 컴포넌트의 가상 돔을 업데이트합니다. 이 과정은 가상 돔의 변경 사항을 계산하는데, 이때 불필요한 렌더링이나 DOM 조작을 최소화하기 위해 효율적인 알고리즘을 사용합니다.
  • 실제 DOM 업데이트
    • 가상 돔의 변경 사항을 계산한 후에는 실제 DOM과 비교하여 변경된 부분만을 찾아내고 실제 DOM을 업데이트합니다. 이 과정에서 브라우저에 불필요한 렌더링이 발생하지 않도록 최적화되어 있습니다.
       

3. 재렌더링 최적화

리액트는 성능 최적화를 위해 다양한 방법을 제공합니다. 이중에서 가장 중요한 것은 다음과 같습니다:
  • PureComponent와 React.memo
    • 순수 컴포넌트(Pure Component)를 사용하여 컴포넌트의 재렌더링을 방지하거나, 함수형 컴포넌트에 React.memo를 사용하여 불필요한 재렌더링을 방지합니다.
  • shouldComponentUpdate 또는 React.memo에 의한 컴포넌트 최적화
    • shouldComponentUpdate 또는 React.memo를 사용하여 컴포넌트의 재렌더링을 제어하고 최적화할 수 있습니다.
  • 불변성 유지
    • 불변성을 유지하여 상태를 변경할 때마다 새로운 객체를 생성하고 이를 통해 불필요한 렌더링을 방지합니다.
 
 
이러한 리액트의 렌더링 과정과 최적화 기법을 이해하면 효율적인 리액트 애플리케이션을 개발할 수 있습니다. 이번 아티클에서는 React의 useRef Hook에 대해 살펴보았습니다. 이번 아티클에서 배운 내용으로 useRef를 적재적소에 활용하여 사용하여 성능이 좋은 애플리케이션을 만들어 나가시길 바라며, 이만 마칩니다.
 
 
 

🚢 개발자 이직 준비, 어떻게 시작해야 할지 모르겠나요? 한 단계 더 도약하는 험난한 항해에서 든든한 메이트가 되어드리겠습니다.

성장의 한계를 느끼고 있는 주니어 개발자들은 항해 플러스와 함께 하시면 됩니다. 기본기 역량 강화부터, 커리어 점프시켜 줄 TDD/성능최적화 프로젝트와 이직 코칭까지 한번에 할 수 있습니다. 성장을 향한 강한 의지만 있다면 항해 플러스 10주 성장 코스로 이직을 도전해보세요.
 
 
 
CREDIT
글 | 소프트웨어 엔지니어 김현명
Share article
Subscribe to our newsletter

IT 커리어 성장 코스, 항해