React의 state중 useState, setState에 대해서 공부해 보았습니다.
export function App() {
const [count, setCount] = React.useState(0);
const handleClick = () => {
setCount((prev) => prev + 1);
};
return (
<>
<div>{count}</div>
<button onClick={handleClick}>증가</button>
</>
);
}
증가버튼을 클릭하면 count가 하나 증가하고 리렌더링된다.
export function App() {
const [parentCount, setCount] = React.useState(0);
const handleClick = () => {
setCount((prev) => prev + 1);
};
return <ChildComponent parentCount={parentCount} onClick={handleClick} />;
}
function ChildComponent({ parentCount, onClick }) {
const [childCount, setChildCount] = React.useState(parentCount);
return (
<>
<div>parentCount : {parentCount}</div>
<div>childCount : {childCount}</div>
<button onClick={onClick}>증가</button>
</>
);
}
증가 버튼을 눌러도 parentCount는 바뀌지만, childCount는 바뀌지 않는다. 부모 컴포넌트가 re-render되서 자식요소에 props를 넘겨주고, 그 props를 받아 useState안에 initialState를 넣어도 childCount는 바뀌지 않는다. 그 이유는 다음과 같다.
다시 말해서, 처음만 빼고는 setChildCount로 업데이트를 해줘야 하기 때문에 아무리 부모컴포넌트에서 props를 useState에 넘겨준다 한들 값이 바뀌지 않는다.
import React, { useEffect } from "react";
export function App() {
const [parentCount, setCount] = React.useState(0);
const handleClick = () => {
setCount((prev) => prev + 1);
};
return <ChildComponent parentCount={parentCount} onClick={handleClick} />;
}
function ChildComponent({ parentCount, onClick }) {
const [childCount, setChildCount] = React.useState(parentCount);
useEffect(() => {
setChildCount(parentCount);
}, [parentCount]);
return (
<>
<div>parentCount : {parentCount}</div>
<div>childCount : {childCount}</div>
<button onClick={onClick}>증가</button>
</>
);
}
부모로부터 받은 props를 childCount로 쓰고 싶은 경우에는 useEffect를 써야한다. 즉 parentCount가 바뀔때마다 setChildCount를 해주는 것이다. hook은 이렇게 작동한다.
그럼 만약에 initialState가 계속 적용되면 어떻게 될까? 이게 과연 문제가 될일인가?
아마도 childCount의 상태가 두곳에서 관리되기 때문에 어떤,,,,먼 미래에,,,사이드 이펙트가 발생할 지도 모르겠다.
function getBigNumber() {
let count = 0;
for (let i = 0; i < 10 ** 9; i += 1) {
count += 1;
}
return count;
}
export function App() {
const bigNumber = getBigNumber();
const [num, _] = React.useState(bigNumber);
const [count, setCount] = React.useState(0);
return (
<>
<div>{num}</div>
<button onClick={() => setCount((prev) => prev + 1)}>리렌더트리거</button>
</>
);
}
리렌더트리거버튼을 누를때마다 getBigNumber는 계속 호출된다. 그런데 이거는 너무 시간이 오래 걸려서 사용자 경험에도 안좋다. 이런 경우 useState안에 함수를 넣을 수 있다.
function getBigNumber() {
let count = 0;
for (let i = 0; i < 10 ** 9; i += 1) {
count += 1;
}
return count;
}
export function App() {
const [num, _] = React.useState(() => getBigNumber());
const [count, setCount] = React.useState(0);
return (
<>
<div>{num}</div>
<button onClick={() => setCount((prev) => prev + 1)}>리렌더트리거</button>
</>
);
}
useState안에 함수 자체를 넣으면 처음에만 실행되고, 그 다음 렌더링때는 실행되지 않는다. 아마도 함수가 들어오면 메모리셀에 값이 없는 경우에만 그 함수를 다시 실행시키는것 같다.
getBigNumber를 다시 실행시키는 방법은 없는걸까?
오! 이런 방법이 있다.
function getBigNumber() {
let count = 0;
for (let i = 0; i < 10 ** 9; i += 1) {
count += 1;
}
return count;
}
export function App() {
const [myKey, setMyKey] = React.useState(1);
return (
<>
<ChildComponent key={`mykey-${myKey}`} />
<button onClick={() => setMyKey((prev) => prev + 1)}>다시그리자</button>
</>
);
}
export function ChildComponent() {
const [num, _] = React.useState(() => getBigNumber());
return <div>{num}</div>;
}
역시 React는 key를 가지고 ReactNode를 비교한다. 다시말해서 key를 바꿨기 때문에 다른 ReactNode로 취급되고 새로 마운트 되는 것이다.
아무튼 핵심은 useState는 함수도 받는다는것! (이것을 lazy initialization이라고 부른다)
function doLongWork() {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1500);
});
}
export function App() {
const [count, setCount] = useState(0);
const handleClick = async () => {
await doLongWork();
setCount(count + 1);
};
return (
<>
<div>{count}</div>
<button onClick={() => handleClick()}>증가</button>
</>
);
}
증가 버튼을 한번 클릭하고 -> 2초 기다렸다가... -> 한번 다시 클릭하고 -> 2초 기다렸다가... -> 보면 카운트가 2가 된다.
그런데 버튼을 2번 연속 클릭하면 1만 증가한다. 그 이유는 다음과 같다.
핵심은 handleClick함수가 실행되는 시점에 count가 무엇이냐 하는 것이다.
그래서 어떻게 해야 이 문제를 해결할까? (엄밀히는 문제는 아니다. 그냥 그렇게 작동할 뿐!)
function doLongWork() {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1500);
});
}
export function App() {
const [count, setCount] = useState(0);
const handleClick = async () => {
await doLongWork();
setCount((prev) => prev + 1);
};
return (
<>
<div>{count}</div>
<button onClick={() => handleClick()}>증가</button>
</>
);
}
setCount((prev) => prev + 1);
이렇게 이전 값을 활용 해야 한다. 이게 작동하는 원리는 다음과 같지 않을까...하고 추측을 해본다.
먼저 심플하게 요약하면, (prev) => prev + 1
이 함수의 첫번째 인자값으로 메모리 셀에 있는 최신값을 넣어주고 prev + 1
을 메모리 셀에 동기적으로 업데이트 한다. 그래서 count도 최신 값을 유지하는 것이다.
prev => prev + 1
요기의 prev에 메모리 셀에 있는 값을 넣는다. 그리고 그 결과 값(prev + 1)을 메모리셀에 다시 동기적으로 업데이트 한다. 그래서 다음 setCount(prev => prev + 1)
가 실행될때 prev에는 메모리 셀에 있는(방금 업데이트한) 값 (= 최신값) 이 들어간다.React는 setState를 비동기적으로 처리한다. setState하자마자 바로 re-render되는것이 아니라 queue에 담아놨다가 state update를 한번에 샤라락 하고 re-render는 한번만 해준다.
export function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("count in useEffect : ", count);
}, [count]);
const handleClick = () => {
setCount((prev) => prev + 1);
console.log("handleClick");
setCount((prev) => prev - 1);
};
return (
<>
<div>{count}</div>
<button onClick={handleClick}>Click Me</button>
</>
);
}
count를 dependency로 하는 useEffect안에 넣어준 함수는 실행되지 않는다. 즉, setCount((prev) => prev + 1);
를 하자마자 re-render가 일어나는게 아니라 setCount((prev) => prev - 1);
이거까지 다 처리되고 나서 re-render가 일어난다. 그렇기 때문에 count는 0 에서 0으로 변했고 console.log("count in useEffect : ", count);
는 실행되지 않는다.
그런데 만약에 setCount를 호출한 직후에 렌더링을 강제하고 싶다면? 이때 바로 flushSync를 사용하면 된다.
export function App() {
console.log("re-render!");
const [count, setCount] = React.useState(0);
const handleClick = () => {
console.log("before flushSync");
flushSync(() => {
console.log("before setCount");
setCount((prev) => prev + 1);
console.log("after setCount");
});
console.log("after flushSync");
const countBox = document.querySelector("#count-box");
console.log("count of state : ", count);
console.log("count in element : ", countBox.textContent);
};
return (
<>
<div id="count-box">{count}</div>
<button onClick={handleClick}>증가</button>
</>
);
}
증가 버튼을 클릭하면 다음과 같은 순서로 로그가 찍힌다
여기서 눈여겨 볼만 한것은 두가지다.
첫번째는, flushSync이후에 DOM이 바로 업데이트 되었다는 것이다. 이것이 flushSync의 용도이다. DOM에 바로 반영이 되기 때문에 setState이후 DOM조작을 할때 유용할것 같다.
두번째는, 함수 컴포넌트 안에 있는 count는 아직 0이라는 것이다. flushSync의 이름이 무시무시하지만 하나의 함수일 뿐이고 이 함수에서 어떤 일을 하던간에, flushSync가 호출된 직후 실행될 console.log("count of state : ", count);
이 시점에서의 count는 flushSync 호출 전 후와 같다. 그래서 4번에 re-render가 되었지만 그것이 이미 실행중인 함수의 count(이전 상태)의 값을 바꾸지는 않는다. 당연한 말인데 설명하려니까 말이 길어진다..
function doSomething() {
const newCount = 1;
justFunction(newCount);
}
function justFunction(newCount) {
const count = newCount;
if (count === 0) {
doSomething();
console.log(count);
}
}
justFunction(0);
정확하진 않지만 대충 이런 느낌인듯 싶다. count는 0이다!
이상입니다. 읽어주셔서 감사합니다 :D