useState, react state 돌아보기 =(Frontend/웹 관련 지식2024. 11. 24. 21:40
Table of Contents
useState
- React로 개발을 하다보면 여러가지 훅을 사용하는데 그 중, 아마 가장 많이 사용하는 훅이 useState일 것이다.
- useState를 이용해서 컴포넌트의 상태를 변화시키면 변화를 감지하여 컴포넌트를 리렌더링하기 위해 씀
- 가끔.. useState 관련해서 깊게 모른다는 생각이 들곤 하는데.. 잘 몰라도 사용법만 알면 잘 작동하기 때문에 그냥 넘어간적이 한두번이 아니였던 것 같기도 하다 ;_;
- useState, react state에 대해 여러가지 관련된 키워드들에 좀 더 자세히 알아보자 =)
- immutability, closure, re-rendering, Fiber, reconciliation, 등등...
- 대충 아는척 할 수는 있지만 제대로 모르는 것 같아서 글로 기록해본다!
useState 기본 지식
useState의 동작 원리
- 초기 상태 설정: useState는 컴포넌트가 렌더링될 때 호출되며, 초기 상태값을 설정한다
- state: 현재 상태 값을 나타낸다.
- setState: 상태를 업데이트하는 함수.
- initialValue: 초기 상태값을 설정.
const [state, setState] = useState(initialValue)
- React 내부에서 상태 관리: React는 내부적으로 컴포넌트의 상태와 관련된 정보를 "state queue"라는 구조로 관리한다.
- useState는 렌더링이 발생할 때 동일한 순서로 호출되어야 하며, React는 호출 순서를 기반으로 각 상태를 연결한다.
- 상태 업데이트: setState 함수를 호출하면, React는 내부적으로 상태를 업데이트하고, 컴포넌트를 다시 렌더링합니다.
- React는 상태값을 즉시 변경하지 않고 배치(batch) 처리합니다. 이는 성능 최적화를 위해 한꺼번에 여러 상태 업데이트를 처리하기 위함이다.
- 상태 업데이트가 비동기적 특성을 가지므로, 새로운 상태값을 기반으로 기존 상태를 변경하려면 함수형 업데이트를 사용한다
setState((prevState) => prevState + 1);
- 렌더링과 관계: 상태가 변경되면 React는 해당 컴포넌트를 다시 렌더링하며, 렌더링 과정에서 새로운 state 값이 반영된다.
상태 업데이트와 리렌더링 원리
React의 상태 관리와 렌더링
- React는 컴포넌트를 "순수 함수"처럼 동작하도록 설계되었다.
- 컴포넌트는 같은 props와 state가 주어지면 항상 동일한 UI를 반환한다.
- 상태(state)가 변경되면 React는 컴포넌트를 다시 호출하여 새로운 UI를 계산한다.
useState가 리렌더링을 트리거하는 방식
- useState의 setState 함수가 호출되면 React는 내부적으로 상태 변경을 감지하고, 해당 상태를 사용하는 컴포넌트를 다시 렌더링한다
- React는 이 작업을 위해 Fiber 구조와 Scheduler API를 활용한다. 이를 통해 상태 변경이 발생한 컴포넌트를 다시 렌더링 대기열에 추가된다.
내부적으로 어떤 API가 작동할까?
- React에서 상태 업데이트 시 리렌더링이 발생하는 이유는 React의 내부 동작 방식 때문이다.
1. Fiber 구조
- React는 모든 컴포넌트를 Fiber 노드라는 데이터 구조로 관리한다.
- 각 컴포넌트는 자신의 상태와 UI를 포함하는 Fiber 객체로 표현된다.
- setState가 호출되면 React는 변경된 상태를 Fiber 트리에 기록하고, 해당 컴포넌트를 다시 렌더링할 준비를 한다.
2. Dirty Checking
- React는 상태 변경이 발생하면 해당 컴포넌트를 "dirty" 상태로 표시한다.
- Dirty 상태로 표시된 컴포넌트만 렌더링 대기열에 추가하여 최적화된 렌더링을 수행한다.
3. Virtual DOM Reconciliation
- useState로 인해 상태가 업데이트되면 React는 새로운 Virtual DOM을 생성하고, 이전 Virtual DOM과 비교한다.
- 이 과정을 "Reconciliation"이라고 하며, 변경된 부분만 실제 DOM에 반영한다.
4. 스케줄링과 렌더링
- React는 상태 변경을 감지하면 렌더링 작업을 스케줄링한다.
- React의 Scheduler는 최적의 렌더링 시점을 결정한다.
- 예를 들어, 유저 인터랙션(클릭, 입력 등)이 중요한 경우 우선 순위를 높이고, 백그라운드 작업은 나중에 처리한다.
setState가 리렌더링을 트리거하는 과정
1. setState 호출
- 사용자가 setState를 호출하면, React는 상태 변경을 Fiber 트리에 기록한다.
2. Fiber 노드 업데이트
- React는 해당 컴포넌트를 "dirty" 상태로 표시한다.
- "dirty" 상태는 이 컴포넌트의 상태나 props가 변경되었음을 의미한다.
3. 렌더링 대기열 추가
- React는 dirty 상태의 컴포넌트를 렌더링 대기열에 추가한다.
4. Virtual DOM 생성, 비교, 업데이트
- React는 컴포넌트를 다시 호출하여 새로운 Virtual DOM을 생성한다.
- 이전 Virtual DOM과 비교하여 변경된 부분만 실제 DOM에 반영한다("Reconciliation").
5. 실제 DOM 업데이트
- React는 변경된 부분만 효율적으로 업데이트한다
불변성(immutability)
불변성은 데이터 구조가 변경되지 않고, 수정이 필요할 때는 기존 값을 복사한 뒤 새로운 데이터로 변경된 구조를 반환하는 원칙이다다.
- 가변성(mutable):
let arr = [1, 2, 3]; arr.push(4); // arr는 [1, 2, 3, 4]로 변경됨
- 불변성(immutable):
let arr = [1, 2, 3]; let newArr = [...arr, 4]; // 새로운 배열 [1, 2, 3, 4] 생성, arr는 변경되지 않음
React와 불변성의 관계!
- React는 상태 관리와 렌더링에서 불변성을 기반으로 최적화를 수행한다.
- 상태가 변경되었는지 확인하기 위해 얕은 비교(shallow comparison)를 사용하며, 이 과정에서 불변성의 원리가 적용된다
React의 상태 비교 방식
- React는 이전 상태와 새로운 상태를 비교하여 상태가 변경되었는지 판단한다.
- 얕은 비교로 이전 상태와 새로운 상태가 다른지 확인한다.
- 새로운 상태가 동일한 참조를 가지면 변경되지 않았다고 판단
- 새로운 상태가 다른 참조를 가지면 변경되었다고 판단
불변성을 유지하지 않을 경우의 문제
- 상태를 직접 수정하면 참조가 동일하게 유지되기 때문에 React는 상태 변경을 감지하지 못하고 컴포넌트를 리렌더링하지 않는다!
const [state, setState] = useState({ count: 0 });
// 상태를 직접 변경 (불변성 위반)
state.count = 1;
setState(state); // React는 상태가 변경되었다고 인식하지 못함
불변성을 유지하는 상태 업데이트 방법
1. 참조형 업데이트
- 참조형 상태를 업데이트할 때는 반드시 새로운 참조형 반환값 생성해야 한다
- 새로운 주소를 가진 값이란 소리에용
const [user, setUser] = useState({ name: "duckgugong", age: 444 });
// 새로운 객체를 생성하여 업데이트
const updateAge = () => {
setUser((prevUser) => ({
...prevUser, // 이전 상태를 복사
age: prevUser.age + 1, // 변경된 부분만 수정
}));
};
2. 원시형 업데이트
- 원시값은 값 자체로 비교된다
- 원시값은 본질적으로 불변성을 가진다.
- 새로운 값을 생성하면 기존 값을 변경하지 않고 새로운 메모리 공간에 저장된다.
- 따라서 변경될 값을 넣기만 하면 올바른 상태 업데이트를 할 수 있다
- 원시값은 본질적으로 불변성을 가진다.
useState와 클로져
- useState와 클로저(closure)는 밀접한 관계가 있다.
- useState는 함수형 컴포넌트에서 사용되며, 상태 관리와 업데이트가 클로저의 특성을 활용하기 때문이다.
클로저란?
- 클로저는 자신을 생성한 스코프(scope)의 변수에 접근할 수 있는 함수를 말한다.
- 즉, 함수가 선언될 당시의 환경(lexical environment)을 기억하여, 해당 스코프의 변수에 접근하거나 이를 유지할 수 있다.
function makeCounter() {
let count = 0; // 외부 변수
return function () {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
- 여기서 count는 makeCounter 함수의 스코프 내에 있지만, 반환된 함수가 클로저로서 이를 계속 기억하고 접근한다.
- 간단한 useState 예시로 볼 수도 있엉
useState와 클로저의 관계
- React의 useState는 함수형 컴포넌트에서 상태를 업데이트하는 방법을 제공하는뎅 이 과정에서 클로저가 중요한 역할을 한다.
상태 유지와 클로저
- useState로 생성된 상태(state)는 함수형 컴포넌트가 다시 렌더링되더라도 초기화되지 않는다
- 이는 useState가 내부적으로 클로저를 사용해 이전 상태 값을 기억하고 관리하기 때문이다
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // `count`는 클로저로 캡처된 값
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
- increment 함수는 렌더링 시마다 새로 생성되지만 count 변수는 클로저로 캡처된 당시의 값을 참조!
클로저로 인한 상태 참조 문제
- useState와 클로저의 관계 때문에, 상태가 최신값이 아닌 이전 값을 참조하는 문제가 생길 수 있습니다.
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(count + 1); // count는 렌더링 당시의 값을 기억
}, 1000);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
- 위 코드는 setTimeout 안에서 클로저로 캡처된 count 값을 사용하기 때문에, 버튼 클릭 후 1초 뒤에 업데이트된 값이 아닌 렌더링 당시의 값을 기준동작한다.
해결 방법: 함수형 업데이트
- 함수형 업데이트를 사용하면 이전 상태를 안전하게 참조할 수 있다.
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount((prevCount) => prevCount + 1); // 최신 상태를 안전하게 업데이트
}, 1000);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
왜 클로저가 중요한가?
- React의 상태 유지 메커니즘:
- React는 컴포넌트가 다시 렌더링되더라도 이전 상태와 함수를 유지한다. 이때 상태와 관련된 함수들이 클로저를 통해 렌더링 당시의 값을 기억한다.
- 상태 동기화:
- setState와 같은 상태 업데이트 함수는 클로저로 동작하며, 이전 상태값을 안전하게 참조하거나 업데이트한다.
- 렌더링과 독립적 로직:
- React의 렌더링이 반복될 때, 상태 업데이트 함수(setState)는 동일한 참조를 유지하며 클로저의 특성을 활용해 상태를 관리한다.
클로저를 이용해서 간단하게 useState 구현해보기
- React처럼 상태를 관리하려면, 클로저와 외부 저장소를 이용해 상태값을 유지해야 한다
function createUseState() {
let state; // 상태값을 저장할 변수
// useState 함수
return function useState(initialValue) {
// 초기화는 최초 호출 시에만 수행
if (state === undefined) {
state = initialValue;
}
// 상태를 업데이트하는 함수
function setState(newValue) {
state = newValue; // 새로운 값으로 상태 변경
console.log("State updated to:", state); // React에서는 여기서 re-rendering 트리거됨
}
return [state, setState];
};
}
// useState 함수 생성
const useState = createUseState();
// 컴포넌트처럼 사용하는 예시
function Counter() {
const [count, setCount] = useState(0);
console.log("Rendered with count:", count);
return {
increment: () => setCount(count + 1),
decrement: () => setCount(count - 1),
};
}
// Counter 컴포넌트의 사용
const counter = Counter(); // 렌더링
counter.increment(); // count + 1
counter.increment(); // count + 1
counter.decrement(); // count - 1
'Frontend > 웹 관련 지식' 카테고리의 다른 글
dist랑 node_module이 뭐였더라.. (1) | 2024.11.24 |
---|---|
ESM (ECMAScript Modules), CJS(common JS) (1) | 2024.11.19 |
javascript IOC (0) | 2024.05.07 |
Semantic Web (시맨틱 웹) (0) | 2022.09.16 |
SPA, MPA, MVC (0) | 2022.09.16 |
@덕구공 :: Duck9s'
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!