React의 useState 훅은 컴포넌트 내부에서 상태를 관리할 수 있게 해주는 가장 기본적인 도구 중 하나다. 상태 관리는 동적인 UI를 구현하는 데 매우 중요하며, 상태의 변화에 따라 컴포넌트가 어떻게 업데이트되고 렌더링되는지 이해하는 것이 핵심이다. 이 글에서는 useState가 어떻게 동작하는지, 그 내부 구조와 상태 관리의 메커니즘을 살펴보고, 효율적인 상태 업데이트 방법에 대해 설명할 것이다.
useState의 기본 개념
useState 훅은 배열을 반환한다. 이 배열에는 두 가지 값이 포함되어 있다: 상태 값과 상태를 변경하는 함수(setter). 이 훅을 사용하면, 상태 값을 설정하고 나중에 업데이트할 수 있는 변수와 그 상태를 업데이트할 수 있는 함수를 얻게 된다. 하지만 중요한 내부적인 메커니즘이 존재한다.
useState의 동작
컴포넌트가 마운트된 후, useState()는 상태를 업데이트하기 위해 내부적으로 updateState()라는 함수 구현체를 사용한다. useState()는 컴포넌트의 상태를 관리하며, 상태가 변경될 때마다 컴포넌트를 리렌더링하는 역할을 한다. 이 과정에서 updateState() 함수가 호출된다.
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
위 코드에서 보듯이, updateState()는 단순히 updateReducer()로 포워딩된다. updateReducer()는 useReducer()와 유사하게 동작하며, useState()와 useReducer()는 내부적으로 같은 메커니즘을 공유한다. 차이점은 useReducer()는 상태 업데이트 시 사용할 리듀서 함수를 외부에서 주입할 수 있다는 것이다.
useState와 useReducer의 차이점
useState()와 useReducer()는 상태 관리를 위한 유사한 패턴을 따르지만, 가장 큰 차이점은 reducer 함수를 외부에서 주입할 수 있느냐에 있다.
- useState()는 기본 리듀서인 basicStateReducer를 사용해 상태를 관리한다.
- useReducer()는 외부에서 상태 업데이트 로직을 주입할 수 있어, 복잡한 상태 관리를 할 때 더 유용하다.
이러한 차이점 덕분에, useReducer()는 복잡한 상태 변경이 필요한 경우에 적합하며, useState()는 단순한 상태 변경에 주로 사용된다.
State 업데이트는 비동기적이다: Batching
React는 이벤트 핸들러 내에서 여러 개의 상태 업데이트가 발생하면 이를 batching(일괄 처리)한다. React는 이벤트 핸들러가 끝날 때까지 상태 업데이트를 처리하지 않고, 그 후에 한꺼번에 처리한다. 이를 통해 불필요한 리렌더링을 방지하고 성능을 최적화할 수 있다.
그러나 중요한 점은 서로 다른 이벤트 간에는 배치가 일어나지 않는다는 것이다. 예를 들어, 클릭 이벤트가 발생할 때마다 배치가 처리되며, 각 클릭 이벤트는 독립적으로 처리된다.
상태와 상태 변수를 결합하는 방법
useState를 사용하여 여러 개의 상태 변수를 관리할 수 있다. 하지만 만약 여러 상태가 서로 밀접하게 연관되어 있다면, 상태 변수를 하나의 객체로 결합하는 것이 더 효율적일 수 있다. 특히, 폼과 같이 여러 필드가 있는 경우 각 필드마다 별도의 상태 변수를 사용하는 대신, 하나의 객체로 관리하면 상태 업데이트가 훨씬 간단해진다.
useState는 내부적으로 queue를 사용한다.
상태가 변경될 때마다 React는 이 업데이트 정보를 queue에 저장한다. queue는 연결 리스트로 구현되며, 여러 번의 setState() 호출은 queue에 쌓인다. 그런 다음 컴포넌트가 리렌더링될 때, queue에 저장된 모든 업데이트가 순차적으로 실행되고, 최종적으로 적용된 상태가 반영된다.
const [number, setNumber] = useState(0);
function handleClick() {
setNumber(number + 5);
setTimeout(() => {
alert(number); // 상태가 즉시 업데이트되지 않음
}, 3000);
}
이 코드를 보면 setNumber(number + 5)를 통해 상태를 업데이트하지만, setTimeout 안에서 number는 즉시 업데이트되지 않는다. 이는 React가 상태 업데이트를 큐에 넣고, 이후 한꺼번에 처리하기 때문이다. 이처럼 state는 snapshot처럼 작동하며, 상태가 변경되기 전의 값을 참조할 수 있다.
local변수 대신 state를 사용하는 이유
상태 관리를 위한 useState를 사용하는 이유는 로컬 변수가 렌더링 간에 유지되지 않기 때문이다. 다음과 같은 두 가지 이유로 로컬 변수가 상태 관리에 적합하지 않다.
- 로컬 변수는 렌더링 간에 지속되지 않는다.
React가 컴포넌트를 두 번째로 렌더링할 때, 로컬 변수를 처음부터 다시 설정한다. 따라서 데이터가 계속해서 사라지게 된다. - 로컬 변수의 변경은 렌더링을 트리거하지 않는다.
React는 로컬 변수가 변경된 것을 감지하지 못하며, 따라서 컴포넌트를 다시 렌더링하지 않는다.
이 문제를 해결하기 위해 useState가 제공된다. 이 훅은 다음 두 가지 기능을 제공한다.
- 렌더링 간 상태 유지: 상태는 컴포넌트가 다시 렌더링될 때도 유지된다.
- 렌더 트리거: 상태가 변경되면 React는 컴포넌트를 다시 렌더링한다.
State는 컴포넌트 간에 독립적이다
상태는 컴포넌트 인스턴스에 고유하며, 다른 컴포넌트와 공유되지 않는다. 예를 들어, 같은 컴포넌트를 여러 번 렌더링하면, 각각의 인스턴스는 완전히 독립적인 상태를 갖는다. 한 컴포넌트의 상태를 변경해도 다른 컴포넌트의 상태에는 영향을 미치지 않는다. 이는 재사용 가능한 컴포넌트를 만들 때 매우 유용하다.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// 여러 Counter를 렌더링하면 상태는 서로 독립적이다.
<Counter />
<Counter />
Reducer를 사용한 상태 관리 최적화
useState를 통해 개별 상태 변수를 관리할 수 있지만, 상태 관리가 복잡해질 경우 Reducer 패턴을 사용하는 것이 더 효과적일 수 있다. Reducer는 상태 업데이트 로직을 하나의 함수로 집중시키며, 상태 변화의 흐름을 명확하게 이해할 수 있게 해준다.
Reducer는 상태와 액션을 받아서 새로운 상태를 반환하는 순수 함수다. 이 함수는 렌더링 중에 실행되며, 상태 업데이트의 복잡성을 줄이는 데 유용하다.
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
Reducer 패턴은 여러 상태 변화를 명확하게 관리할 수 있어, 특히 복잡한 상태 로직을 처리할 때 유용하다. 상태 업데이트 로직을 한 곳에 모아 관리함으로써 코드의 가독성과 유지보수성을 높일 수 있다.
'React' 카테고리의 다른 글
React Hook 이해하기 (3): useRef (0) | 2024.09.10 |
---|---|
React Hook 이해하기 (2): useEffect (0) | 2024.09.10 |
React에서의 Event Handler (0) | 2024.09.10 |
React와 JSX (0) | 2024.09.05 |
React에서 조건부 렌더링 (0) | 2024.09.02 |