React 에서 state 를 쓸 줄 모른다는 건 React를 다룰 줄 모른다는 뜻이 아닐까.
state, React를 처음 입문했을 당시 나에게는 너무나 생소한 개념이었다. 굳이 지역 함수가 아닌 별도의 state 라는 개념을 채용한 이유가 무엇인가? 이런 나의 의문은 React의 생태계를 공부하면서 하나씩 풀렸다.
하지만 아직도 나는 state를 완벽히 이해했다고 말하기 어렵다. 함수형 업데이트를 진행하면 동기적으로 작동하는 게 아닌가? 라는 잘못된 생각을 최근에야 바로잡게 된 이후 이러한 생각은 더욱 심화되었다.
따라서 날을 잡아서 state와 관련된 개념을 최대한 깨부수고, 디테일한 설명의 경우에는 추가적으로 React 코드를 좀 뜯어보면서 작동 원리를 이해하고자 한다. 하지만 React 소스 코드의 복잡도란.. 이하 생략..
state
라고 정의하였다.state
값이 변경되었을 경우 React 는 해당 컴포넌트를 리렌더링 한다. 따라서 state
의 변화는 컴포넌트의 리렌더링을 유발시킨다고 볼 수 있다.+1
, -1
버튼을 눌렀을 경우 이에 맞춰서 count 변수의 값을 변경시키는 이벤트 핸들러를 버튼에 할당시켰다.0
이 찍힌다.function Counter() {
let count = 0;
function increaseCount() {
count += 1;
console.log(count);
}
function decreaseCount() {
count -= 1;
console.log(count);
}
return (
<div>
<button onClick={increaseCount}>+1</button>
<button onClick={decreaseCount}>-1</button>
<p>Count : {count}</p>
</div>
);
}
export default Counter;
count
가 컴포넌트가 새롭게 리렌더링 될 때마다 0
으로 초기화 되기 때문이다. React는 지역 변수의 값이 변경되어도 이를 리렌더링에 반영하지 않는다.console.log
에는 count의 변경 값이 잘 찍히지만 리렌더링이 되지 않았기 때문에 화면에는 컴포넌트가 초기에 렌더링 되었을 때의 count
값인 0을 그대로 유지한다.state
로 구현하였다.useState
훅은 컴포넌트가 기억하고자 하는 값, 즉 state
를 컴포넌트에 추가할 수 있도록 해주는 React Hook 이다.useState
은 state와 setter 함수를 하나의 배열로 반환하여 넘겨주며, 일반적으로 비구조화 할당을 통해 두 개의 값을 인계 받는다.import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function increaseCount() {
setCount(count + 1);
console.log(count);
}
function decreaseCount() {
setCount(count + 1);
console.log(count);
}
return (
<div>
<button onClick={increaseCount}>+1</button>
<button onClick={decreaseCount}>-1</button>
<p>Count : {count}</p>
</div>
);
}
export default Counter;
useState
훅으로 재구성한 코드이다. 이제 정상적으로 카운터가 작동된다!import Counter from "./Counter";
function App() {
return (
<div>
<Counter />
<Counter />
</div>
);
}
export default App;
state
값이 변경되었을 경우 React 에서는 해당 컴포넌트를 리렌더링 하며, 불필요한 리렌더링을 방지하기 위해 state를 변경하는 작업을 일괄적으로 처리한다.state
의 업데이트 작업을 모아 일괄 처리하는 방식을 Batching 이라고 하며, 이 덕에 React 에서는 불필요한 리렌더링을 방지할 수 있게 되었다.function changeState() {
const [flag, setFlag] = useState(false);
// setFlag (setter) 함수가 세 차례 실행되었지만, 리렌더링은 단 한번만 발생하게 된다.
setFlag((prevFlag) => !prevFlag); // false => true
setFlag((prevFlag) => !prevFlag); // true => false
setFlag((prevFlag) => !prevFlag); // false => true
}
Batching 에 대해 더 자세히 기술한 포스팅은 아래에 있다.
import { useState } from "react";
function Counter() {
// 초기 렌더링 시 count 값은 0으로 고정된다. 이는 다음 렌더링 이전까지 절대 변경되지 않는다.
const [count, setCount] = useState(0);
function increaseCount() {
// 초기 렌더링 이후 count 값은 0으로 고정된다.
setCount(count + 1); // 0 + 1 = 1
setCount(count + 1); // 0 + 1 = 1, 이전의 결과에 영향을 받지 않고 현재 count 값을 적용한다.
setCount(count + 1); // 0 + 1 = 1, 이전의 결과에 영향을 받지 않고 현재 count 값을 적용한다.
}
return (
<div>
<button onClick={increaseCount}>+3</button>
<p>Count : {count}</p>
</div>
);
}
export default Counter;
0
에서 1
로 변경된다.count
state 값은 초기 렌더링을 진행한 후 0으로 고정되었다. 따라서 setCount(count + 1)
작업의 결과는 0에 1을 더한 값으로 state를 업데이트 할 것이다.count
값은 현재 0으로 고정된 상태기에 결국 여러번 작업을 수행해도 리렌더링 이후 결과는 1로 변경될 것이다.import { useState } from "react";
function Counter() {
// 초기 렌더링 시 count 값은 0으로 고정된다. 이는 다음 렌더링 이전까지 절대 변경되지 않는다.
const [count, setCount] = useState(0);
function increaseCount() {
// 리렌더링 이후 count 값은 3으로 변경된다. 현재 count 값은 0임에 유의
setCount(count + 100); // 0 + 100 = 100
setCount(count - 200); // 0 - 200 = -200
setCount(count + 1); // 0 + 1 = 1, 최종적으로 count 값은 1로 수정되어 다음 렌더링에 반영됨
}
return (
<div>
<button onClick={increaseCount}>+1</button>
<p>Count : {count}</p>
</div>
);
}
export default Counter;
1
로 변경되었다.0
임을 유의해야 한다. 세 번의 setCount
함수가 호출되었으나 인자로 받은 count 값은 0
으로 고정된다.setCount
의 결과가 최종적으로 변경된 count 값이 되고, 리렌더링 이후 count 값은 1
으로 수정되는 것이다.import { useState } from "react";
function Counter() {
const [number, setNumber] = useState(0);
return (
<div>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 5);
// setTimeout 함수를 통해 비동기적으로 alert 함수를 3초 후 호출시켰음
// 하지만 해당 작업이 스케줄러에 등록된 당시의 state 값인 0이 적용된다.
setTimeout(() => {
alert(number);
}, 3000);
}}
>
+5
</button>
</div>
);
}
export default Counter;
number
state 에 5를 더하고, setTimeout 함수를 사용하여 3초 후에 number
state를 alert 창에 띄우도록 하였다.0
이 띄워진다. 왜냐하면 alert 함수에 인자로 들어간 number는 사용자가 버튼을 클릭했을 때의 state 값을 담았기 때문이다.import { useState } from "react";
function Counter() {
// 초기 렌더링 시 count 값은 0으로 고정된다. 이는 다음 렌더링 이전까지 절대 변경되지 않는다.
const [count, setCount] = useState(0);
function increaseCount() {
setCount(prev => prev + 1); // 0 + 1 = 1, 초기 렌더링 이후 count 값은 0으로 고정된다.
setCount(prev => prev + 1); // 1 + 1 = 2, 상단의 updater function의 return 값인 1을 인계 받았다.
setCount(prev => prev + 1); // 2 + 1 = 3, 상단의 updater function의 return 값인 2을 인계 받았다.
return (
<div>
<button onClick={increaseCount}>+3</button>
<p>Count : {count}</p>
</div>
);
}
export default Counter;
import { useState } from "react";
function App() {
// 초기 렌더링 시 count 값은 0으로 고정된다. 이는 다음 렌더링 이전까지 절대 변경되지 않는다.
const [count, setCount] = useState(0);
function increaseCount() {
// 리렌더링 이후 count 값은 1으로 변경된다. 현재 count 값은 0임에 유의
setCount(count + 100); // 0 + 100 = 100
setCount((prevCount) => prevCount + 1); // 100 + 1 = 101
setCount((prevCount) => prevCount + 1); // 101 + 1 = 102
setCount(count + 1); // 0 + 1 = 1, 최종 count 값은 1으로 변경
}
return (
<div>
<button onClick={increaseCount}>+1</button>
<p>Count : {count}</p>
</div>
);
}
export default App;
React 에서 추천하는 updater function 의 Name Convention
setEnabled((e) => !e);
setLastName((ln) => ln.reverse());
setFriendCount((fc) => fc * 2);
prev
같은 접두사를 붙이거나 아니면 state 변수의 이름을 그대로 사용해도 된다고 한다.useState
훅을 사용할 때 인자로 넣어준 값은 초기 렌더링 시에 React 에 저장되고, 이후의 렌더링에서는 초기에 저장했던 값을 그대로 사용한다.function MyApp({ Component, pageProps }: AppProps) {
const [queryClient] = useState(
new QueryClient({
defaultOptions: {
queries: {
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<Provider>
<GlobalStyle />
<ThemeProvider theme={theme}>
<ModalPortal />
<Component {...pageProps} />
</ThemeProvider>
</Provider>
</QueryClientProvider>
);
}
export default MyApp;
function MyApp({ Component, pageProps }: AppProps) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<Provider>
<GlobalStyle />
<ThemeProvider theme={theme}>
<ModalPortal />
<Component {...pageProps} />
</ThemeProvider>
</Provider>
</QueryClientProvider>
);
}
export default MyApp;
initialValue
에 함수를 실행시키지 말고 함수 자체를 인자로 넣어보자. 초기 렌더링 시에만 함수가 호출되고 이후에는 반환된 값을 저장하여 활용하니 훨씬 좋다.