React 에서 상태(state)는 렌더링 될 때 고정됩니다
- React에서 사용자가 버튼을 클릭하면 화면에 보이는 UI가 즉시 변경된다
- setState 함수가 직접적으로 상태를 업데이트 하고 렌더링을 한다
위 내용은 맞는 문장일까요 틀린 문장일까요?
화면만 보았을 때는 맞는 문장이 될 수 있지만 React 내부 동작을 봤을 때는 틀린 문장입니다.
state(상태)는 일반 함수의 내부에 선언된 지역 변수와 달리 컴포넌트 외부에 변수를 저장하는 메모리와 같이 동작하기 때문입니다. 마치 선반에 두고 필요할 때(렌더링할 때) 꺼내 사용하는 것과 유사한 모습이라 할 수 있습니다. React는 state(상태)를 이용하여 매 렌더링 마다 이벤트 핸들러, props를 고정된 state(상태)를 이용하여 계산합니다.
import { useState } from 'react';
export default function Form() {
const [isSent, setIsSent] = useState(false);
const [message, setMessage] = useState('Hi!');
if (isSent) {
return <h1>Your message is on its way!</h1>
}
return (
<form onSubmit={(e) => {
e.preventDefault();
setIsSent(true);
sendMessage(message);
}}>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}
function sendMessage(message) {
// ...
}
출처: https://beta.reactjs.org/learn/state-as-a-snapshot
해당 코드는 다음과 같은 순서로 동작하게 됩니다.
- onSubmit 이벤트 핸들러가 실행됩니다.
- setIsSent(true)가 isSent를 true로 업데이트 하고 setIsSent를 큐에 넣습니다.
- React가 새롭게 업데이트 된 isSent 를 활용하여 해당 컴포넌트를 리렌더링 합니다.
정리해보겠습니다.
사용자가 클릭을 하여 이벤트 핸들러 함수가 실행되면서 setter 함수(setState라고 칭하겠습니다.)가 실행이 됩니다. 여기서 React가 업데이트 된 상태를 이용하여 컴포넌트를 리렌더(re-render) 했을 때 사용자는 화면을 볼 수 있게 됩니다.
이 과정을 하나로 생각하지 않고, 다음과 같이 두가지 과정으로 나누어 생각해볼 수 있습니다.
1. setState 함수를 실행하여 상태(state)를 업데이트 하는 과정
2. React가 리렌더(re-render) 하는 과정
따라서 앞서 질문했던 "React에서 사용자가 버튼을 클릭하면 화면에 보이는 UI가 바로 변경된다"에 대한 내용을 React 렌더링 내부 동작의 관점에서 다시 살펴보면 setState함수가 직접적으로 UI를 변경하는 것이 아닌 유발(trigger)할 뿐이라는 것을 알 수 있습니다.
setState 함수에 대해서
아래 코드를 실행하고 +3버튼을 클릭하면 어떤 결과가 나올까요?
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
출처: https://beta.reactjs.org/learn/state-as-a-snapshot
아마 number가 3으로 변경될 것이라 예상할 수 있지만 결과는 1이 됩니다. 앞서 우리는 setState 함수는 앞으로 진행하게 될 렌더링에 사용될 상태(state)를 변경하기만 한다는 것을 알 수 있었습니다. 해당 내용을 기준으로 동작 순서를 살펴보겠습니다.
- 처음 렌더링이 될 때 number = 0 입니다
- +3 버튼을 클릭하면 setNumber(numer + 1) 함수가 실행됩니다. 이때, +3 버튼을 처음 클릭하는 순간의 numer의 값은 0 이기 때문에 setNumber(0 + 1)이라 할 수 있습니다.
- +3 버튼을 처음 클릭할 때, 모든 setNumber에서 사용되는 number 값은 0이 때문에 setNumber(0 + 1)을 동일하게 3번 실행하는 것과 같습니다.
정리하자면, 각 렌더링 마다 고정된 상태(state) 값을 사용합니다.
React의 상태(state)는 객체의 형태로 관리됩니다.
React는 Object.assign메서드를 활용하여 state(상태)를 관리합니다. 해당 메서드는 동일한 키 값을 갖고 있는 경우 덮어씌우기 때문에 setNumber(number + 1)을 3번 실행해도 number의 값은 1이 됩니다. 이러한 과정을 batching 이라 합니다.
Object.assign(target, ...sources)
const currentState = {
number: 1
};
const newState = Object.assign(currentState, { number: 1 }, { number: 1 });
setCount(newState)
그렇다면, 위의 숫자를 증가시키는 코드 예제에서 +3 버튼을 한 번 클릭하는 것으로 number를 3으로 증가시키고 싶다면 어떻게 해야할까요? 즉 다시말해서 최신의 상태(state) 값을 리렌더링이 일어나기 전에 읽어 오려면 어떻게 해야할까요? 이는 setState 함수의 인자로 함수를 넣으면 해결할 수 있습니다.
setState 함수의 인자로 함수를 넣으면..?
setState 함수의 인자에 함수를 넣는 것으로 이전 값을 사용하여 다음 상태에 반영할 수 있습니다.
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
</>
)
}
출처: https://beta.reactjs.org/learn/state-as-a-snapshot
setNumber함수에 인자로 함수(n => n + 1)를 넣으면 다음과 같은 동작으로 진행됩니다.
- React가 setNumber함수에 들어온 인자 n => n + 1이 함수인 것을 확인합니다.
- 호출된 setState(n => n + 1) 함수를 순서대로 큐에 넣습니다.
- onClick 이벤트 핸들러의 모든 코드가 실행된 후에 상태값을 활용 하여 n => n + 1에 대한 연산을 처리합니다.
순서를 정리해보면,
- 컴포넌트가 렌더링 될 때, state(상태)는 고정됩니다.
- +3 버튼을 클릭하여 이벤트 핸들러 함수가 실행될 때의 number 는 모두 0 입니다.
- setNumber를 통해 number를 각각 업데이트 하지 않고 batching을 통해 한 번에 업데이트 합니다.
- setNumber의 인자로 함수가 들어왔기 때문에, 호출된 순서대로 큐에 n => n + 1 함수를 넣습니다.
- React가 렌더링을 할 때, 이전 number의 값(초기 렌더링시)은 0이었기 때문에 첫 n => n + 1 함수의 인자 n에 0을 전달하고 그 결과 값으로 1을 반환합니다.
- 2번 더 해당 작업을 반복하고 마지막에 3을 반환 합니다.
queued updatenreturns
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
Hook 내부 동작 원리
- setState 인자로 함수가 들어왔는지 체크하는 로직
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
- setState 함수가 호출되기 전의 hook 상태
{
memoizedState: 0,
baseState: 0,
queue: {
last: null,
dispatch: dispatchAction.bind(bull, currenctlyRenderingFiber$1, queue),
lastRenderedReducer: basicStateReducer(state, action),
lastRenderedState: 0,
},
baseUpdate: null,
next: null
}
- queue: (setNumber(n + 1))
last: {
//...otherOptions 생략
action: n + 1,
eagerReducer: basicStateReducer(state, action),
eagerState: n + 1,
next: {
last: {
//...otherOptions 생략,
action: n + 1,
eagerReducer: basicStateReducer(state, action),
eagerState: n + 1,
next: null
}
}
},
{
memoizedState: 0,
baseState: 0,
queue: {
last: {
expirationTime: 1073741823,
suspenseConfig: null,
action: 1, // setNumber 를 통해 설정한 값
eagerReducer: basicStateReducer(state, action),
eagerState: 1, // 실제로 상태 업데이트를 마치고 렌더링되는 값
next: { /* ... */},
priority: 98
},
dispatch: dispatchAction.bind(bull, currenctlyRenderingFiber$1, queue),
lastRenderedReducer: basicStateReducer(state, action),
lastRenderedState: 0,
},
baseUpdate: null,
next: null
}
- queue: (setNumber(n => n + 1))
last: {
//...otherOptions 생략
action: n => n + 1,
eagerReducer: basicStateReducer(state, action),
eagerState: n + 1,
next: {
last: {
//...otherOptions 생략,
action: n => n + 1,
eagerReducer: basicStateReducer(state, action),
eagerState: (n + 1) + 1,
next: null
}
}
},
- eagerState: React의 Batching Process를 통해 최종적으로 업데이트 될 상태
- eagerReducer: action을 통해 eagerState를 계산
참고
https://beta.reactjs.org/learn/queueing-a-series-of-state-updates
'FRONTEND' 카테고리의 다른 글
useCallback (0) | 2024.12.14 |
---|---|
React Hooks Introduction & useState (0) | 2024.12.13 |
useEffect (0) | 2024.12.11 |
[NextJS] 공식문서 읽어보기 Learn - SEARCH ENGINE OPTIMIZATION - Crawling and Indexing (0) | 2023.05.26 |
[NextJS] 공식문서 읽어보기 Learn - SEARCH ENGINE OPTIMIZATION - Introduction to SEO (0) | 2023.04.29 |