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
useRef를 사용해 mutable ref 객체인 countRef를 생성하고, 초기값을 0으로 설정
이 객체는 .current라는 프로퍼티를 통해 접근
◼︎ useEffect로 mount/unmount 로그
useEffect는 컴포넌트가 처음 mount될 때와 unmount될 때 각각 로그를 출력
useState의 초기값과 useRef의 .current 값을 로그로 출력
return 문 안에 있는 unmount 함수는 컴포넌트가 사라질 때 마지막 상태 값을 출력
컴포넌트가 처음 렌더링될 때 countState와 countRef의 초기값인 0이 각각 로그로 출력된다.
💡 로그 순서
Mounted: useState 0 & Mounted: useRef 0
컴포넌트가 처음 화면에 나타날 때(mount), useState 값이 0인 상태로 시작
그리고 useRef도 초기값 0을 가지고 있어 출력
Unmounted: useState 0 & Unmounted: useRef 0
그다음 React가 컴포넌트를 잠시 화면에서 지움(unmount). 이때 마지막 상태인 useState와 useRef 값을 출력
다시 Mounted: useState 0 & Mounted: useRef 0
React가 컴포넌트를 다시 화면에 보여줌(remount). 이때 또다시 useState와 useRef가 0으로 초기화된 상태로 출력
이 과정은 주로 개발할 때 일어나며, React가 컴포넌트를 제대로 동작하는지 확인하기 위해 일부러 한 번 지웠다가 다시 그리는 것이다. 이 기능은 Fast Refresh나 React.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에 의한 이중 렌더링의 영향을 받지 않는다.
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 반영을 효율적으로 관리할 수 있는 솔루션을 제공한다.
[React] useState vs useRef
React를 사용해 프로젝트를 진행하다 보면, 사용자의 입력을 받아 UI에 반영하는 작업이 필수적이다. 특히 폼(form) 요소와 같이 input 필드의 값을 어떻게 관리할지에 대한 고민을 자주 하게 된다. 이때 주로 사용하는 두 가지 Hook이 바로 useState와 useRef이다. 두 Hook은 상태를 관리하는 방식과 리렌더링에 대한 차이점이 있기 때문에 상황에 따라 적절히 선택해야 한다.
리액트를 처음 시작하는 사람들은 useState로 모든 값을 관리하려는 경향이 있지만, 경우에 따라서는 useRef가 더 적합한 선택일 수 있다. 이번 포스팅에서는 useState와 useRef의 차이점과 각 훅을 언제 사용하는 것이 더 효율적인지에 대해 알아보고, 특정 상황에서 어떤 기준으로 선택할지에 대해 다뤄보려 한다.
01. useState
React에서 컴포넌트는 자신의 상태(state)나 props가 변경되면 리렌더링된다.
상태를 관리하기 위해 React에서는 useState Hook을 사용한다.
useState는 상태 유지 값과 그 값을 갱신하는 함수를 반환한다.
첫번째 값인 상태값(state)는 현재 상태를 반환한다.
setState 함수는 상태를 업데이트 할 수 있는 함수로, 새 state를 받아 컴포넌트 리렌더링 큐에 등록한다. 즉 호출 시 컴포넌트가 다시 리렌더링된다. 컴포넌트는 다음 렌더링 시에 useState를 통해 반환받은 첫번째 값은 항상 갱신된 최신 state가 된다.
💡 특징
02. useRef
useRef는 DOM 요소나 값의 참조를 관리하기 위한 Hook으로, 주로 DOM에 직접 접근하거나 렌더링과 무관한 값을 저장하는데 사용된다.
useRef는 초기 값(initialValue)을 인자로 받아, 그 값으로 .current 프로퍼티를 초기화 한 mutable ref 객체를 반환한다.
위 코드 예시에서 useRef(null)는 current가 null로 초기화 된 객체를 생성한다.
useRef는 순수한 자바스크립트 객체를 생성하며, 이는 리렌더링과 무관하게 값을 저장할 수 있는 mutable 상자 역할을 한다.
즉, useRef로 만든 객체를 수정하는 것은 컴포넌트의 렌더링과 무관하며, useRef로 생성된 객체의 .current 값을 변경하더라도 컴포넌트는 리렌더링 되지 않는다.
예시 코드에서 textInput.current.focus()는 DOM에 직접 접근해 input 필드를 포커싱하지만, 이는 렌더링에 영향을 주지 않는다.
💡 특징
03. useState vs useRef
다음 useState와 useRef 사용 코드 예시를 보면 렌더링 차이를 더 명확히 알 수 있다.
◼︎ 초기 상태와 ref 값 설정
◼︎ useEffect로 mount/unmount 로그
컴포넌트가 처음 렌더링될 때 countState와 countRef의 초기값인 0이 각각 로그로 출력된다.
💡 로그 순서
이 과정은 주로 개발할 때 일어나며, React가 컴포넌트를 제대로 동작하는지 확인하기 위해 일부러 한 번 지웠다가 다시 그리는 것이다. 이 기능은 Fast Refresh나 React.StrictMode 때문에 일어난다. 실제 배포된 사이트에서는 이런 일이 일어나지 않는다. 이와 관련된 내용은 다음 포스팅인 useEffect에서 더 자세히 다룬다.
🎯 useState로 상태 업데이트
updateCountState 함수는 setCountState를 호출하여 상태 값을 1씩 증가시킨다.
setCountState는 이전 상태 값(prevCount)을 이용해 렌더링 전에 로그를 출력한다. 그래서 처음 Before Render (useState): 0이 출력된 것이다. 이후 상태가 업데이트되면 컴포넌트가 리렌더링된다. 상태가 바뀐 후에 화면에서 버튼에 표시된 값도 업데이트된다.
즉, 버튼을 클릭할 때마다 이전 상태 값이 출력되고, 컴포넌트가 리렌더링되어 화면에 표시된 useState 값이 증가한다.
개발 모드에서 React.StrictMode는 컴포넌트를 두 번 렌더링한다. 이중 렌더링을 통해 사이드 이펙트가 올바르게 처리되는지 확인한다.이중 렌더링으로 인해 useState의 상태 업데이트 로그가 두 번 출력될 수 있다.
🎯 useRef로 상태 업데이트
updateCountRef 함수는 useRef로 생성한 countRef.current 값을 1씩 증가시킨다. useRef는 리렌더링 없이 값을 즉시 업데이트하며, 변경된 값은 다음번 버튼 클릭 시 화면에 반영된다. 따라서, 버튼을 클릭하면 countRef.current 값이 증가하지만, 컴포넌트는 리렌더링되지 않는다. 콘솔 로그를 통해서만 값을 확인할 수 있고, 화면의 내용은 변하지 않는다.
useRef는 컴포넌트의 렌더링을 유발하지 않으므로, React.StrictMode에 의한 이중 렌더링의 영향을 받지 않는다.
로그가 두 번 출력되지 않고, useRef의 값만 업데이트되는 것을 볼 수 있다.
04. 회원가입 form에서 useState와 useRef
#1. useState 이용
사용자에게 입력 받는 mail과 결과를 출력하는 result를 각각 useState로 관리하는 예시이다.
이메일 입력 필드의 값이 변경될 때마다 setMail을 호출하여 컴포넌트를 렌더링하며, 폼 제출 시 result를 업데이트하여 결과를 표시하게 된다. 위 코드를 수행한 결과 아래와 같다.
input에 입력을 할 때마다 렌더링이 실행되는 것을 볼 수 있다.
#2. useRef 이용
mailRef를 사용하여 이메일 입력 값을 관리한다. mailRef.current의 값은 입력 필드의 값이 변경될 때 업데이트되지만, 컴포넌트는 렌더링되지 않는다. 폼 제출 시 mailRef.current 값을 result 상태로 설정하여 결과를 표시한다.
useRef를 사용했을 때 render는 2번 실행되었다. 첫번째는 맨 처음 화면이 렌더링되었을때, 두번째는 회원가입 버튼을 눌러 setEmail로 상태가 변경되어 리렌더링 되었을 때 실행되었다. 아까 useState로 input값을 수정할때와 달리 useRef를 사용한 결과 render가 더 적게 일어나는 것을 알 수 있다.
05. useState와 useRef 사용 시기
◼︎ useState
사용자 입력을 제어하고, 입력 값에 따라 UI를 즉시 반영할 필요가 있는 경우, 예를 들어, 입력 값에 따라 스타일을 변경(입력 값에 따라 border 변경 등)하거나, UI 요소를 보여주거나 숨겨야 할 때 사용하는 것이 좋다.
◼︎ useRef
입력 값의 변화를 상태와 UI에 직접적으로 반영하지 않고, 렌더링을 방지하고 싶을 때. 자주 변경되는 값을 추적하되, 렌더링을 방지하여 성능을 최적화하고 싶을 때 사용하는 것이 좋다.
'ASAC > Front-End' 카테고리의 다른 글