
리액트에서 훅이란 함수형 컴포넌트에서 사용할 수 있는 편리한 기능들이다.
어떤 종류들이 있는지 알아보자!
useStateuseState는 상태 state와 상태를 변경하는 함수 setState를 생성한다.
setState는 Batch 업데이트를 사용하기 때문에 비동기적으로 동작한다.
🪄복습! React와 Batch 업데이트
React는 성능 최적화를 위해 상태 업데이트를 하나씩 차례대로가 아니라 Batch로 수행한다. 그래서 같은 영역에setState가 여러번 호출이 되더라도 React는 스케쥴링을 통해 이들을 모아 한 번만 렌더링을 하게 된다.
setStatesetState에 넣는 값은 새로운 상태가 되어 결국 상태가 업데이트된다. 다만 어떤 값을 넣느냐에 따라 동작 방식이 살짝 달라진다.
리액트는 Batch 업데이트를 위해 전달된 값을 즉시 반영하지 않고, 대기열에 추가한다. 이 과정에서 setState는 비동기적으로 동작하기 때문에 상태 업데이트가 완료되기 전에는 새로운 상태를 모른다. 그래서 대기열에 추가되는 새 값들은 초기 상태값을 기준으로 계산된다.
const [count, setCount] = useState(0);
setCount(count + 1); // count는 여전히 0인 상태에서 계산됨 → 1
setCount(count + 1); // count는 여전히 0인 상태에서 계산됨 → 1, 첫 업데이트가 아직 반영 x
// count =====> 1
함수를 전달하면 이전 상태를 사용할 수 있게 된다! setState 내부에서 최신 상태를 계산하여 제공하기 때문에 각 함수 호출에서 최신 상태를 참조할 수 있다. 그래서 실제로는 비동기적으로 동작하지만, 마치 동기적으로 동작하는 것 처럼 보이게 된다.
const [count, setCount] = useState(0);
// 인자로 이전 상태가 들어간다.
setCount(prevCount => prevCount + 1); // prevCount는 0 → 1
setCount(prevCount => prevCount + 1); // prevCount는 1 → 2
// count =====> 2
Q. 순차적으로 상태가 최신 상태로 업데이트 된다는 것인데, 이것도 Batch업데이트라고 할 수 있나요?
A. 그렇다! 여전히 Batch 업데이트다! 아래 예시를 보자.
const [state, setState] = useState(0);
const handleClick = () => {
setState(prev => prev + 1);
setState(prev => prev + 1);
setState(prev => prev + 1);
};
// 클릭 시 state는 0 에서 3이 된다.
리액트는 Batch 업데이트를 위해 setState에 전달된 함수들을 대기열에 모아놓고, 렌더링이 한 번 발생한다. 다만 각 setState에서 최신 상태가 계산이 되기 때문에 이 핸들러를 실행하면 상태값은 한 번에 3이 증가하게 된다.
따라서 Batch 업데이트가 적용되면서도 최신 상태를 순차적으로 계산이 가능한 것이다! 여러번 setState가 호출되더라도 한 번의 렌더링으로 최종 업데이트가 적용된다.
useState의 초기값useState의 초기값도 값을 전달하냐, 함수를 전달하냐에 따라 동작이 조금 달라진다.
const [state, setState] = useState(초기값);
리렌더링마다 초기값이 다시 평가된다.
단순한 값이라면 큰 문제없지만, 아주 복잡한 함수 호출 결과가 들어간다면 심각한 성능 문제가 발생할 수 있다.
// 계산이 무거울 때
const [value] = useState(computeExpensiveValue(prop)); // 매 렌더링 시 재계산
// 로컬 스토리지 접근도 비용이 적지 않다.
const [data] = useState(localStorage.getItem('key')); // 매 렌더링 시 실행
const [state, setState] = useState(() => 초기화_함수());
컴포넌트가 처음 마운트(생성)될 때 단 한번만 평가된다.
그 이후에 아무리 리렌더링하더라도 절대 재실행되지 않는다.
그래서 무거운 연산이나 동적 초기화(브라우저 저장소에서 불러온 값으로 초기화하는 등)가 필요할 때 최적화에 유용하다!
// 복잡한 계산
const [value] = useState(() => computeExpensiveValue(prop));
// 로컬 스토리지에서 데이터 읽기
const [data] = useState(() => {
const saved = localStorage.getItem('key');
return JSON.parse(saved) || [];
});
Q. 값이 평가된다는 것이 값이 초기화된다는 것인가요?
A. 아니다! 평가는 값을 계산한다는 뜻이고, 초기화는 되지 않는다. 리액트는 이전 상태 값을 내부적으로 저장하고 유지하기 때문에 리렌더링 되더라도 초기화되지 않는다. But! 초기값을 다시 평가하긴 한다. 평가된 결과가 반영되지 않을 뿐이다. 함수를 전달할 경우 평가를 최초 1회만 하고 다시는 하지 않는다. 그래서 무거운 연산의 경우 함수가 더 효율적인 것이다.
리렌더링은 변경이 일어나는 상태가 있는 컴포넌트(와 그 하위 컴포넌트들)에서 일어나는 것이다. 상위 컴포넌트들은 아무 변화가 없다!
또한 리액트의 렌더링이란 가상 DOM을 렌더링한다는 뜻이다. 렌더링이 끝난 후 변경사항이 있는 부분들을 batch 방식으로 실제 DOM을 업데이트한다.
아래의 경우를 살펴보자.
function Parent() {
const [count, setCount] = useState(0); // 상태가 렌더링에 사용되지 않음
return <Child onUpdate={() => setCount(count + 1)} />;
}
부모 컴포넌트는 상태 count가 렌더링에 영향을 주지 않는다. 그렇다 하더라도 자식 컴포넌트에서 setCount를 통해 count를 변경시키면 부모 컴포넌트는 일단 리렌더링 된다. 가상 돔은 리렌더링 되지만, 실제 돔을 업데이트 하지 않는 것이다!