[React] useState vs useRef

 

 

React를 사용해 프로젝트를 진행하다 보면, 사용자의 입력을 받아 UI에 반영하는 작업이 필수적이다. 특히 폼(form) 요소와 같이 input 필드의 값을 어떻게 관리할지에 대한 고민을 자주 하게 된다. 이때 주로 사용하는 두 가지 Hook이 바로 useState와 useRef이다. 두 Hook은 상태를 관리하는 방식과 리렌더링에 대한 차이점이 있기 때문에 상황에 따라 적절히 선택해야 한다.

리액트를 처음 시작하는 사람들은 useState로 모든 값을 관리하려는 경향이 있지만, 경우에 따라서는 useRef가 더 적합한 선택일 수 있다. 이번 포스팅에서는 useState와 useRef의 차이점과 각 훅을 언제 사용하는 것이 더 효율적인지에 대해 알아보고, 특정 상황에서 어떤 기준으로 선택할지에 대해 다뤄보려 한다.

 

01. useState

React에서 컴포넌트는 자신의 상태(state)나 props가 변경되면 리렌더링된다.

상태를 관리하기 위해 React에서는 useState Hook을 사용한다.

 

const [state, setState] = useState(initialState);

useState는 상태 유지 값과 그 값을 갱신하는 함수를 반환한다.

첫번째 값인 상태값(state)는 현재 상태를 반환한다.

setState 함수는 상태를 업데이트 할 수 있는 함수로, 새 state를 받아 컴포넌트 리렌더링 큐에 등록한다. 즉 호출 시 컴포넌트가 다시 리렌더링된다. 컴포넌트는 다음 렌더링 시에 useState를 통해 반환받은 첫번째 값은 항상 갱신된 최신 state가 된다.

 

💡 특징

  • 렌더링 유발 : useState로 관리하는 값이 변경되면 컴포넌트가 리렌더링
  • 상태 변경 함수 제공 : 상태를 갱신할 수 있는 함수를 반환하며, 이 함수를 호출해 상태를 변경
  • 초기 값 지정 : useState는 인자로 초기 상태를 받으며, 이는 컴포넌트가 처음 렌더링될 때만 설정
  • UI에 반영되는 상태 관리 : 사용자 인터페이스에 즉각 반영되거나 상태 변화를 감지해 UI 업데이트가 필요한 데이터에 적합

 

 

 

 

02. useRef

useRef는 DOM 요소나 값의 참조를 관리하기 위한 Hook으로, 주로 DOM에 직접 접근하거나 렌더링과 무관한 값을 저장하는데 사용된다.

function CustomTextInput(props) {
  const textInput = useRef(null); // 초기값을 null로 설정한 ref 객체 생성

  function handleClick() {
    textInput.current.focus(); // 버튼 클릭 시 input에 포커스
  }

  return (
    <div>
      <input type="text" ref={textInput} /> 
      <button onClick={handleClick}>click me</button>
    </div>
  );
}

 

useRef는 초기 값(initialValue)을 인자로 받아, 그 값으로 .current 프로퍼티를 초기화 한 mutable ref 객체를 반환한다.

위 코드 예시에서 useRef(null)는 current가 null로 초기화 된 객체를 생성한다.

useRef는 순수한 자바스크립트 객체를 생성하며, 이는 리렌더링과 무관하게 값을 저장할 수 있는 mutable 상자 역할을 한다.

즉, useRef로 만든 객체를 수정하는 것은 컴포넌트의 렌더링과 무관하며, useRef로 생성된 객체의 .current 값을 변경하더라도 컴포넌트는 리렌더링 되지 않는다.

예시 코드에서 textInput.current.focus()는 DOM에 직접 접근해 input 필드를 포커싱하지만, 이는 렌더링에 영향을 주지 않는다.

 

 

💡 특징

  • 렌더링을 유발하지 않음 : useRef로 저장된 값이 변경되더라도, 컴포넌트는 리렌더링 되지 않음
  • DOM 요소 참조 : useRef는 주로 DOM 요소에 접근하기 위해 사용. 예를 들어, 특정 input 필드를 포커싱하거나, 비디오 재생을 제어하는 경우 등.
  • 렌더링과 무관한 값 저장 : 렌더링이 필요 없는 데이터를 저장하거나, 컴포넌트가 리렌더링되어도 값을 유지하고 싶을 때 유용
  • 값의 유지 : 컴포넌트가 리렌더링 되더라도 useRef에 저장된 값은 사라지지 않고 그대로 유지
 
useRef를 바람직하게 사용할 수 있는 사례는 다음과 같다.
 
  • DOM 조작: 포커스, 텍스트 선택, 미디어 재생 등을 제어할 때
  • 애니메이션: 직접적으로 애니메이션을 실행하거나 제어할 때
  • 서드파티 라이브러리: 외부 DOM 라이브러리와 함께 사용할 때
  • 렌더링과 관계없는 상태 관리: 렌더링에 영향을 주지 않는 값을 저장할 때

 

 

 

 

03. useState vs useRef

다음 useState와 useRef 사용 코드 예시를 보면 렌더링 차이를 더 명확히 알 수 있다.

import { useEffect, useRef, useState } from 'react'

function ReactHooks() {
  const [countState, setCountState] = useState<number>(0)
  const countRef = useRef<number>(0)

  // Mount 시 상태와 ref의 초기값을 로그로 확인
  useEffect(() => {
    console.log('Mounted: useState', countState)
    console.log('Mounted: useRef', countRef.current)

    // Unmount 시에도 마지막 값을 확인
    return () => {
      console.log('Unmounted: useState', countState)
      console.log('Unmounted: useRef', countRef.current)
    }
  }, [])

  // useState 업데이트
  const updateCountState = () => {
    setCountState((prevCount) => {
      console.log('Before Render (useState):', prevCount)
      return prevCount + 1
    })
  }

  // useRef 업데이트
  const updateCountRef = () => {
    countRef.current += 1
    console.log('Updated useRef (No Render):', countRef.current)
  }

  return (
    <div className='wrapper'>
      <h1>React Hooks: useState vs useRef</h1>
      <div className='card'>
        <button onClick={updateCountState}>useState count is {countState}</button>
      </div>
      <div className='card'>
        <button onClick={updateCountRef}>useRef count is {countRef.current}</button>
      </div>
    </div>
  )
}

export default ReactHooks

 

◼︎ 초기 상태와 ref 값 설정

  • const [countState, setCountState] = useState<number>(0);
    • useState를 사용해 상태(countState)를 0으로 초기화
    • setCountState는 상태를 업데이트하는 함수
  • const countRef = useRef<number>(0);
    • useRef를 사용해 mutable ref 객체인 countRef를 생성하고, 초기값을 0으로 설정
    • 이 객체는 .current라는 프로퍼티를 통해 접근

 

◼︎ useEffect로 mount/unmount 로그

  • useEffect는 컴포넌트가 처음 mount될 때와 unmount될 때 각각 로그를 출력
  • useState의 초기값과 useRef의 .current 값을 로그로 출력
  • return 문 안에 있는 unmount 함수는 컴포넌트가 사라질 때 마지막 상태 값을 출력

컴포넌트가 처음 렌더링될 때 countState와 countRef의 초기값인 0이 각각 로그로 출력된다.

 

💡 로그 순서

  1. Mounted: useState 0 & Mounted: useRef 0
    • 컴포넌트가 처음 화면에 나타날 때(mount), useState 값이 0인 상태로 시작
    • 그리고 useRef도 초기값 0을 가지고 있어 출력
  2. Unmounted: useState 0 & Unmounted: useRef 0
    • 그다음 React가 컴포넌트를 잠시 화면에서 지움(unmount). 이때 마지막 상태인 useState와 useRef 값을 출력
  3. 다시 Mounted: useState 0 & Mounted: useRef 0
    • React가 컴포넌트를 다시 화면에 보여줌(remount). 이때 또다시 useState와 useRef가 0으로 초기화된 상태로 출력

 

이 과정은 주로 개발할 때 일어나며, React가 컴포넌트를 제대로 동작하는지 확인하기 위해 일부러 한 번 지웠다가 다시 그리는 것이다. 이 기능은 Fast RefreshReact.StrictMode 때문에 일어난다. 실제 배포된 사이트에서는 이런 일이 일어나지 않는다. 이와 관련된 내용은 다음 포스팅인 useEffect에서 더 자세히 다룬다.

 

 

 

 

 

🎯 useState로 상태 업데이트

useState Count 버튼 클릭 시(updateCountState 함수 실행)

updateCountState 함수는 setCountState를 호출하여 상태 값을 1씩 증가시킨다.

setCountState는 이전 상태 값(prevCount)을 이용해 렌더링 전에 로그를 출력한다. 그래서 처음 Before Render (useState): 0이 출력된 것이다. 이후 상태가 업데이트되면 컴포넌트가 리렌더링된다. 상태가 바뀐 후에 화면에서 버튼에 표시된 값도 업데이트된다.

즉, 버튼을 클릭할 때마다 이전 상태 값이 출력되고, 컴포넌트가 리렌더링되어 화면에 표시된 useState 값이 증가한다.

 

개발 모드에서 React.StrictMode는 컴포넌트를 두 번 렌더링한다. 이중 렌더링을 통해 사이드 이펙트가 올바르게 처리되는지 확인한다.이중 렌더링으로 인해 useState의 상태 업데이트 로그가 두 번 출력될 수 있다.

 

 

 

 

 

🎯 useRef로 상태 업데이트

useRef Count 버튼 클릭 시(updateCountRef 함수 실행)

updateCountRef 함수는 useRef로 생성한 countRef.current 값을 1씩 증가시킨다. useRef는 리렌더링 없이 값을 즉시 업데이트하며, 변경된 값은 다음번 버튼 클릭 시 화면에 반영된다. 따라서, 버튼을 클릭하면 countRef.current 값이 증가하지만, 컴포넌트는 리렌더링되지 않는다. 콘솔 로그를 통해서만 값을 확인할 수 있고, 화면의 내용은 변하지 않는다.

 

useRef는 컴포넌트의 렌더링을 유발하지 않으므로, React.StrictMode에 의한 이중 렌더링의 영향을 받지 않는다.

로그가 두 번 출력되지 않고, useRef의 값만 업데이트되는 것을 볼 수 있다.

 

 

 

 

 

04. 회원가입 form에서 useState와 useRef

#1. useState 이용

import React, { useState, FormEvent } from 'react'

const SignUpState: React.FC = () => {
  const [mail, setMail] = useState<string>('')
  const [result, setResult] = useState<string>('')

  console.log('render')

  const onSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    setResult(mail)
  }

  return (
    <div id='container'>
      <form onSubmit={onSubmit}>
        <label htmlFor='email'>
          <span>이메일 주소</span>
          <input
            type='email'
            id='email'
            name='email'
            value={mail}
            onChange={(e) => setMail(e.target.value)}
          />
        </label>
        <span>결과: {result}</span>
        <button type='submit'>회원가입</button>
      </form>
    </div>
  )
}

export default SignUpState

사용자에게 입력 받는 mail과 결과를 출력하는 result를 각각 useState로 관리하는 예시이다.

이메일 입력 필드의 값이 변경될 때마다 setMail을 호출하여 컴포넌트를 렌더링하며, 폼 제출 시 result를 업데이트하여 결과를 표시하게 된다. 위 코드를 수행한 결과 아래와 같다.

 

input에 입력을 할 때마다 렌더링이 실행되는 것을 볼 수 있다.

 

 

 

#2. useRef 이용

import React, { useRef, useState, FormEvent } from 'react'

const SignUpRef: React.FC = () => {
  const mailRef = useRef<string>('')
  const [result, setResult] = useState<string>('')

  console.log('render')

  const onSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    setResult(mailRef.current)
  }

  return (
    <div id='container'>
      <form onSubmit={onSubmit}>
        <label htmlFor='email'>
          <span>이메일 주소</span>
          <input
            type='email'
            id='email'
            name='email'
            onChange={(e) => (mailRef.current = e.target.value)}
          />
        </label>
        <span>결과: {result}</span>
        <button type='submit'>회원가입</button>
      </form>
    </div>
  )
}

export default SignUpRef

mailRef를 사용하여 이메일 입력 값을 관리한다. mailRef.current의 값은 입력 필드의 값이 변경될 때 업데이트되지만, 컴포넌트는 렌더링되지 않는다. 폼 제출 시 mailRef.current 값을 result 상태로 설정하여 결과를 표시한다.

 

useRef를 사용했을 때 render는 2번 실행되었다. 첫번째는 맨 처음 화면이 렌더링되었을때, 두번째는 회원가입 버튼을 눌러 setEmail로 상태가 변경되어 리렌더링 되었을 때 실행되었다. 아까 useState로 input값을 수정할때와 달리 useRef를 사용한 결과 render가 더 적게 일어나는 것을 알 수 있다.

 

 

 

 

05. useState와 useRef 사용 시기

◼︎ useState

사용자 입력을 제어하고, 입력 값에 따라 UI를 즉시 반영할 필요가 있는 경우, 예를 들어, 입력 값에 따라 스타일을 변경(입력 값에 따라 border 변경 등)하거나, UI 요소를 보여주거나 숨겨야 할 때 사용하는 것이 좋다.

  • 장점:
    • UI 동기화: 상태 변경이 UI에 즉시 반영되며, 상태 변화에 따라 UI가 자동으로 업데이트
    • 간결한 코드: 상태 관리와 UI 업데이트가 명확히 이루어져 코드가 간단

 

 

 

◼︎ useRef

입력 값의 변화를 상태와 UI에 직접적으로 반영하지 않고, 렌더링을 방지하고 싶을 때. 자주 변경되는 값을 추적하되, 렌더링을 방지하여 성능을 최적화하고 싶을 때 사용하는 것이 좋다.

  • 장점:
    • 성능 최적화: 값이 변경되더라도 컴포넌트가 다시 렌더링되지 않아 성능이 향상
    • DOM 직접 접근: 필요 시 DOM에 직접 접근하여 값을 읽거나 수정 가능
  • 단점:
    • UI 반영 어려움: 값의 변경이 UI에 반영되지 않기 때문에 UI 변경이 필요할 경우 추가적인 DOM 조작이 필요
    • 코드 복잡성: 상태와 UI 동기화가 명시적으로 이루어지지 않아 코드가 복잡해질 수 있음

 

 

 

+) React-Hook-Form
react-hook-form은 폼 처리에서 발생할 수 있는 문제를 해결하기 위해 설계된 라이브러리이다. 이 라이브러리는 다음과 같은 문제를 해결한다.

- 렌더링 최적화: 폼 입력 값이 자주 변경되는 상황에서도 불필요한 렌더링을 방지하여 성능을 최적화. 이는 useRef를 내부적으로 활용하여 입력 값의 변경이 컴포넌트 렌더링을 유발하지 않도록 하기 때문.
- 폼 상태 관리: 복잡한 폼의 상태를 효율적으로 관리하며, 폼 필드의 검증, 오류 처리, 값의 추적 등을 간편하게 처리.
- 코드 간결성: 폼 관련 코드의 양을 줄이고, 상태 관리와 폼 처리 로직을 명확하게 유지할 수 있도록 도움.

결론적으로, react-hook-form은 복잡한 폼 처리에서 성능을 최적화하고 코드의 가독성을 높이며, 사용자 입력에 따른 상태 변경과 UI 반영을 효율적으로 관리할 수 있는 솔루션을 제공한다.

 

반응형