useState vs useRef

hyunjine·2022년 7월 24일
74
post-thumbnail

React 프로젝트를 진행하면서 input의 값을 관리할 때 useState로 관리할 것인가, useRef로 관리할 것인가에 대해 팀원과 토론했습니다.

이 글은 그 토론에 기반합니다.

먼저, useState와 useRef를 간단하게 비교하는 것으로 시작해보겠습니다.

useState

React에서 컴포넌트는 자신의 상태 또는 props가 바뀌면 리렌더링됩니다.
상태를 관리하기 위해 React에서는 useState를 활용합니다.

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

useState는 상태 유지 값과 그 값을 갱신하는 함수를 반환합니다. setState 함수는 새 state를 받아 컴포넌트 리렌더링 큐에 등록합니다.

컴포넌트는 다음 렌더링 시에 useState를 통해 반환받은 첫번째 값은 항상 갱신된 최신 state가 됩니다.

useRef

Ref는 render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근하는 방법을 제공합니다.

function CustomTextInput(props) {
  // textInput은 ref 어트리뷰트를 통해 전달되기 위해서
  // 이곳에서 정의되어야만 합니다.
  const textInput = useRef(null);

  function handleClick() {
    textInput.current.focus();
  }

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

React 공식 문서에 의하면 ref의 바람직한 사용 사례는 다음과 같습니다.

  • 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때
  • 애니메이션을 직접적으로 실행시킬 때
  • 서드 파티 DOM 라이브러리를 React와 같이 사용할 때

결국 React에서 ref는 DOM을 조작하기 위해 사용됩니다.
하지만 ref를 다른 용도로 사용할 수도 있습니다. 아래와 예시를 보겠습니다.

const refContainer = useRef(initialValue);

useRef.current 프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환합니다.

useRef는 순수 자바스크립트 객체를 생성합니다.
또한 useRef로 만든 객체를 수정하는 것은 컴포넌트의 렌더링과 무관합니다.
다시 말하면, .current 프로퍼티를 변형하는 것이 리렌더링을 발생시키지 않습니다.

본질적으로 useRef.current 프로퍼티에 변경 가능한 값을 담고 있는 상자와 같습니다.
useRef는 상자와 같으므로 useState처럼 컴포넌트 내의 변수 값을 조회, 수정하는 방법으로도 사용할 수 있습니다.

위 두 사례에 의하면, useRef는 일반적으로 특정 DOM을 지정하여 해당 돔의 속성값을 파악하거나 속성값을 변화시키는 용도로 사용할 수도 있고, 순수 자바스크립트 객체를 반환하기 때문에 값을 저장하는 상자로 사용할 수도 있습니다.

useState vs useRef

useStateuseRef의 사용을 비교해보면 렌더링에서 차이점을 보입니다.

먼저 useState를 사용해서 input을 만들어 테스트를 해보겠습니다.

function Input() {
  const [value, setValue] = useState("")
  return <input value={value} onChange={(e) => setValue(e.target.value)} />
}

export default Input

당연하게도 상태가 바뀔 때마다 리렌더링되는 모습을 볼 수 있습니다.

이제 useRef를 사용해서 input 컴포넌트를 테스트 해보겠습니다.

function Input() {
  const inputRef = useRef(null)
  return <input ref={inputRef} />
}

export default Input

애초에 상태로 관리하지 않으므로 리렌더링이 일어나지 않습니다.

여기서 고민했던 점은 input 값을 변경할 때 렌더링이 필요한가에 대한 부분이었습니다.

팀원과 제 생각이 같았던 부분은 input을 입력하는 과정에서 렌더링이 이렇게 많이 일어나야하나?라는 생각이었습니다.

반면 팀원과 제 생각이 달랐던 부분은 다음과 같습니다.

  • 이현진: 'input의 입력값 또한 어플리케이션의 상태이기 때문에 useState로 관리해야한다'
  • 팀원: 'input 입력값은 상태가 아니기 때문에 useRef로 리렌더링을 막아야한다.'

결국 input 값을 관리할 때 useState vs useRef라는 질문은 React에서 input값이 상태인가라는 질문으로 귀결됩니다.

생각

제 생각은 아래와 같습니다.

상태란 주어진 시간에 대해 시스템을 나타내는 것으로 언제든지 변경될 수 있는 것입니다.
사용자가 입력하는 값 또한 어플리케이션의 상태라고 할 수 있고, 시간의 흐름에 따라 변하는 input의 값은 useState로 관리해야합니다.

input을 state로 관리할 때 발생하는 리렌더링에 대한 부분에서는 과연 그 input을 리렌더링하는게 고비용 연산인가라는 의문을 제기하고 싶습니다. input을 타이핑할 때 발생하는 렌더링은 고비용 연산이 아니라고 생각합니다.

반면 useRef로 만들어진 ref객체는 DOM에 접근할 때나, 매 렌더링시에 만들어줘야하는 고비용 객체나 값을 저장할 때 사용하는 것이 옳다고 생각합니다.

결론은 input에서 발생하는 사용자의 상호작용 또한 어플리케이션의 상태이므로 상태를 상태답게 관리하기 위해 useState를 사용해야한다. 입니다.

여러분은 어떻게 생각하시나요?

22.08.03 추가

Controlled/Uncontrolled Components

이 글에서 했던 고민이 공식문서에 나와있었습니다.

우리의 고민은 이 컴포넌트가 Controlled Component인가 Uncontrolled Component인가에 대한 고민이었습니다.

HTMLElement중에는 상태를 가지고 있는 것들이 있습니다.

  • input
  • select
  • textarea

이 HTMLElement들의 상태를 누가 관리하느냐에 따라 Controlled ComponentUncontrolled Component로 나뉩니다.

엘리먼트를 가지고 있는 컴포넌트가 관리한다면, Controlled Component

엘리먼트의 상태를 관리하지 않고 엘리먼트의 참조만 컴포넌트가 소유한다면, Uncontrolled Component입니다.

즉, 쉽게 말하면 useState에 의해 상태로 관리하고 있는 컴포넌트를 제어 컴포넌트라고 하고, React가 상태로 추적하고 있지 않은 컴포넌트를 비제어 컴포넌트라고 합니다. 비제어 컴포넌트같은 경우 ref를 활용해 실제 DOM에 접근합니다. 비제어 컴포넌트는 DOM자체에서 데이터가 다뤄집니다.

아래는 공식문서에서 인용한 문장입니다.

In a controlled component, form data is handled by a React component.
The alternative is uncontrolled components, where form data is handled by the DOM itself.

(번역)
대부분 경우에 폼을 구현하는데 제어 컴포넌트를 사용하는 것이 좋습니다.
제어 컴포넌트에서 폼 데이터는 React 컴포넌트에서 다루어집니다.
대안인 비제어 컴포넌트는 DOM 자체에서 폼 데이터가 다루어집니다.

form의 input 상태같은 경우 React가 추적해서 그 값으로 어떤 행동을 할 여지가 있습니다. 예를들어서 로그인 유효성 검사 로직이 state가 변함에 따라 실행되어야하면 제어 컴포넌트를 활용할 수 있습니다.

반면에, 비제어 컴포넌트같은 경우는 실제 DOM을 참조해야하는 경우에 필요합니다. 가장 흔한 경우로 input에 focus를 하는 상황을 예로 들 수 있습니다.

따라서, 실제 DOM에 접근해야하는 상황이 아니라면 React 컴포넌트가 input의 상태를 관리해야 합니다.(제어 컴포넌트)

더 읽을거리

20개의 댓글

comment-user-thumbnail
2022년 7월 25일

input의 상태를 useState로 관리할 때 겪게 되는 렌더링의 문제점은 보통 여러가지 입력이 존재하는 form을 컨트롤 하는 경우 하나의 input의 상태변화가 페이지 전체의 렌더링을 유발할 수 있다는 점이지 않을까 싶습니다! 이를 보완하기 위해서 memo 등 여려가지 요소를 적용해 볼 수 있겠지만 이를 위해 또 코드가 지나치게 복잡해진다는 단점이 있느 것 같아요 ㅠㅠ

2개의 답글
comment-user-thumbnail
2022년 7월 27일

좋은 글입니다 :)

1개의 답글
comment-user-thumbnail
2022년 7월 31일

넘 유익한 글입니다 ㅎㅎ 잘 보고 갑니다

1개의 답글
comment-user-thumbnail
2022년 8월 1일

input 이 담겨있는 form 컴포넌트의 랜더링 최적화를 잘 해놓은 상태에서 input의 value 를 useState 로 관리하는건 괜찮은 방법인 것 같습니다.
단순히 하나의 input 컴포넌트의 리랜더링 비용은 저비용 연산이겠지만, 랜더링 최적화가 되어있지 않은 상태라면 하나의 input 으로 인해서 그 외 컴포넌트들이 리랜더링 되면서 퍼포먼스에 악영향을 끼칠것 같습니다!

1개의 답글
comment-user-thumbnail
2022년 8월 2일

현재 리액트 공부 중인데 좋은 글 잘 읽었습니다!!😀

1개의 답글
comment-user-thumbnail
2022년 8월 2일

좋은 글 감사합니다! 공식 문서에 state 사용을 권장하여 이유는 생각하지 않고 있었는데 한번 더 생각하게 해주시네요ㅎㅎ

1개의 답글
comment-user-thumbnail
2022년 8월 2일

controlled, unControlled 컴포넌트에 대해 알아보면 좋을거 같습니다~

1개의 답글
comment-user-thumbnail
2022년 8월 17일

저는 FormData라는 되게 신기한 API를 최근에 발견해서 form 내에서 useState, useRef 사용 일절 없이 input 값 관리에 쓰고 있는데 코드도 짧아지고 단순해서 잘 쓰고 있어요. 한 번 봐주시면 감사하겠습니다. https://velog.io/@ddui/FormData

1개의 답글
comment-user-thumbnail
2022년 10월 16일

안녕하세요 현진님 좋은 글 잘 읽었습니다ㅎㅎ 저는 개인적으로 이 문제는 경우에 따라 유동적으로 쓰는게 좋다고 생각합니다. 예를 들자면, 저같은 경우에는 복잡한 form을 다루는 경우 form 전체 상태와 input에는 ref를 사용합니다. 그리고 나서 onChange로 input의 value를 디바운싱하여 전체 상태를 업데이트하는 방식으로 input의 상태값을 기록하여 validation에 사용하고 렌더링은 최소화하는 편입니다. 반면 페이지에 간단한 form형식이 존재한다면 useState로 관리하는 편입니다! 의견 있으시면 남겨주세요!

1개의 답글