컴포넌트는 상호 작용의 결과로 화면의 내용을 변경해야되는 경우가 많습니다.
(Ex. 폼에 입력하면 입력 필드가 업데이트 되어야하고, 캐러셀에서 다음을 클릭할 때 표시되는 내용이 변경되어야 하고, 구매를 클릭하였을 때 상품이 장바구니에 담겨야 함.)
컴포넌트는 현재 입력값과 현재 이미지, 장바구니와 같은 것들을 "기억"해야 합니다.
React는 이런 종류의 컴포넌트별 메모리를 state라고 부릅니다.
유저가 상호작용을 하면 그에 맞게 UI가 변경되어야하는 경우에
컴포넌트는 그 상호작용에 맞는 것들을 "기억"해서 보여주어야 하는 경우가 생깁니다.
React에서는 그 데이터들을 state라고 부릅니다.
컴포넌트의 최상위 레벨에서 useState를 호출하여 state 변수를 선언합니다.
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Taylor');
const [todos, setTodos] = useState(() => createTodos());
// ...
배열 구조 분해를 사용해 [something, setSomething] 과 같은 state 변수의 이름을 지정하는 것이 규칙입니다.
inititalState : state의 초기 설정값이며, 어떤 유형의 값이든 지정할 수 있지만 함수에 대해서는 특별한 동작이 있습니다. 이 인수는 초기 렌더링(mount) 이후에는 무시됩니다.initialState로 전달하면 이를 초기화 함수로 취습합니다.useState는 정확히 두 개의 값을 가진 배열을 반환합니다.
const [state, setState] = useState("initialState");
initialState와 일치합니다.set 함수입니다.useState는 Hook이므로 컴포넌트의 최상위 레벨이나 직접 만든 Hook(custom Hook)에서만 호출할 수 있습니다.setState(nextState)와 같은 set 함수useState 가 반환하는 set 함수를 사용하면 state를 다른 값으로 업데이트하고 리렌더링을 하게 만들 수 있습니다. 여기에는 다음 state를 직접 전달하거나, 이전 state로부터 계산한 함수를 전달할 수도 있습니다.
const [name, setName] = useState('Edward');
function handleClick() {
setName('Taylor');
setAge(a => a + 1);
// ...
nextState : state가 될 값입니다. 값은 모든 데이터 타입이 허용되지만, 함수에 대해서는 특별한 동작이 있습니다.nextState로 전달하면 업데이터 함수로 취급합니다. 이 함수는 순수해야 하고,// age가 42라고 가정하였을때 아래 함수는 예상대로 동작하지 않는다.
// 이는 set 함수를 호출해도 이미 실행 중인 코드에서 age state 변수가 업데이트 되지 않기 때문이다!
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
// 따라서 setAge에 업데이터 함수를 전달해야 한다.
// 여기서 a => a + 1은 업데이터 함수이며, 이 함수는 대기 중인 state를 가져와서 다음 state를 계산한다.
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
set 함수는 반환값이 없습니다.
set 함수는 다음 렌더링에 대한 state 변수만 업데이트합니다.
set 함수를 호출한 후에도 state 변수에는 여전히 호출 전 화면에 있던 이전 값이 담겨 있습니다.
(그 이유는 state가 스냅샷처럼 동작하기 때문인데, 이에 대해서는 다음 포스팅에서 더 자세히 알아보겠습니다.)
사용자가 제공한 새로운 값이 Object.is에 의해 현재 state 와 동일하다고 판정되면,
React는 컴포넌트와 그 자식을들 리렌더링 하지 않습니다.
이것이 바로 최적화이며, 경우에 따라 React가 자식을 건너뛰기 전에 컴포넌트를 호출해야 할 수도 있지만,
코드에 영향을 미치지는 않습니다.
( 매우매우 중요한 부분이라고 생각되는데, Object.is() 메서드로 리렌더링 여부를 파악한다고 한다.
이는 불변성을 유지하며 새로운 데이터를 set 해줄때 spread operator 와 함께 사용해 메모리에서 같은 객체를 참조하지 않게하고, 그로 하여금 객체가 바뀐 상태를 React에게 알려주어 리렌더링을 하게하는 이유인 것이다! )
React는 state 업데이트를 batch 합니다. 모든 이벤트 핸들러가 실행되고 set 함수를 호출한 후에 화면을 업데이트합니다. 이렇게 하면 단일 이벤트 중에 여러번 리렌더링 하는 것을 방지할 수 있습니다.
드물지만 DOM에 접근하기 위해 React가 화면을 더 일찍 업데이트하도록 강제해야 하는 경우,
flushSync 를 사용할 수 있습니다.
렌더링 도중 set 함수를 호출하는 것은 현재 렌더링 중인 컴포넌트 내에서만 허용됩니다.
React는 해당 출력을 버리고 즉시 새로운 state로 다시 렌더링을 시도합니다.
이 패턴은 거의 필요하지 않지만 이전 렌더링의 정보를 저장하는 데 사용할 수 있습니다.
Strict Mode에서 React는 의도치 않은 실수를 찾기 위해 업데이터 함수를 두 번 호출합니다.
이는 개발 환경 전용 동작이며 프로덕션 환경에는 영향을 미치지 않습니다.
만약 업데이터 함수가 순수하다면 동작에 영향을 미치지 않습니다. 호출 중 하나의 결과는 무시됩니다.
(업데이터 함수도 마찬가지)
마운트시 왜 로그가 두번 찍히는지, state를 업데이트하였는데도 이전값이 계속 출력되는지,
리렌더링이 발생하는 조건이 무엇인지에 대해 더욱 정확히 알게되었습니다.
리서치를 하며 다른 포스트를 보는것보다 공식 문서를 한 번 읽는게 정확하고 빠르게 정보를 얻는 방법임을 다시 한 번 느꼈습니다.
위에서 어느정도 훅에대해 정리하였지만 정확히 왜 이렇게 작동하는지, 사용은 어떻게 하는지에 대해서도 정리를 하며 공부하는 과정이 필요할 것 같습니다.
다음 포스팅에서도 역시 공식문서와 함께 useState 훅을 더욱 깊게 파헤쳐보겠습니다.