React Docs Beta API를 보고 학습한 내용입니다.
useState
는 컴포넌트에 상태 변수를 생성할 수 있는 React Hook이다.
React로부터 useState
를 가져온 후 컴포넌트의 최상위 레벨에서 선언하여 컴포넌트에 state를 추가한다.
useState
는 배열을 반환하는데 그것을 구조 분해를 해서 주로 사용한다.
import { useState } from 'react';
const App = () => {
// 컴포넌트의 최상위 레벨에 위치
const [state, setState] = useState(initialState)
// ...
}
배열의 구조분해할당(Array Destructuring)이란?
Array의 값 또는 Object의 속성을 개별 변수로 압축을 해제할 수 있게 해주는 JavaScript 표현식
const [a, b] = [10, 20]; console.log(a) // 10 console.log(b) // 20
state
: '현재' 상태의 값, 컴포넌트가 최초로 렌더링될 때 initialState
가 할당되며 이후 업데이트 시 initialState
를 무시하고 업데이트된 값이 할당된다.
setState
: state의 값을 변경하는 set 함수이다.
[state, setState]는 컨벤션으로 관습적으로 state의 이름에 set을 붙여 useState를 사용한다.
initialState
: 컴포넌트가 최초 렌더링시 state에 자동으로 할당되는 초기 값이다. 초기 값으로 어떤 타입이든 들어올 수 있다.
set 함수를 호출하면 state의 값을 업데이트를 해서 반영하기 위해 컴포넌트가 다시 렌더링된다.
아래의 예시는 버튼을 클릭하면 state의 값이 업데이트되면서 App 컴포넌트가 다시 렌더링하는지 보기 위한 예제이다.
import { useState } from 'react';
const App = () => {
const [state, setState] = useState('상태');
const onClick = () => {
setState('업데이트된 상태');
};
console.log('App 컴포넌트 렌더링');
return <button onClick={onClick}>{state}</button>;
};
export default App;
결과는 아래처럼 작성한 로그가 2번 출력되는 것을 볼 수 있다.
첫번째는 최초 렌더링 시에 출력되었고 두번째는 state가 업데이트된 후에 출력된 것이다.
만약 객체를 state로 한다고 가정해보자.
const [obj, setObj] = useState({ name : "현우", age : 30, gender : "male" });
여기서 객체를 업데이트하려면 업데이트할 프로퍼티만 작성하면 안된다. 만약 업데이트할 프로퍼티만 작성하면 아래처럼 기존의 객체를 업데이트할 객체로 완전히 오버라이드 되기 때문에 반드시 업데이트를 하지 않는 프로퍼티도 작성을 해줘야한다.
const [obj, setObj] = useState({ name : "현우", age : 30, gender : "male" });
setObj({ name: "철수" });
console.log(obj); // { name : "철수" }
그래서 기존의 객체를 업데이트를 편하게 하기 위해 스프레드 연산자(전개 연산자)를 사용하면 된다.
const [obj, setObj] = useState({ name : "현우", age : 30, gender : "male" });
setObj({ ...obj, name: "철수" });
console.log(obj); // { name : "철수", age : 30, gender : "male" }
스프레드 연산자를 반드시 앞에 작성해야 업데이트할 프로퍼티의 값을 새로운 값으로 업데이트할 수 있다.
만약 뒤에 작성하면 기존의 상태와 동일한 상태를 가지게 된다.
업데이트할 새로운 상태 값인 name : "철수"
를 기존의 상태 값인 name : "현우"
가 덮어버리기 때문이다.
setObj({ name: "철수", ...obj });
console.log(obj); // { name : "현우", age : 30, gender : "male" }
useState
의 set 함수를 이용해서 이전 상태를 기반으로 상태를 업데이트 할 수 있다.
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
위의 예시를 보면 상태인 컴포넌트 내부의 상태인 age
는 초기값이 42
라고 가정할 때 hanleClick
함수를 호출 시 42에 1이 세 번 더해져 45
가 되는 것이 아닌 1이 한번만 더해진 43
이 된다.
이것은 set 함수를 호출했을때 React가 '다음 렌더링'에 state를 업데이트를 하려고 준비하기 때문이다.
즉, 위의 함수의 순서는 이렇다.
42
이므로 setAge(42+1)
이며 React가 '다음 렌더링'에 새로운 state의 값으로 업데이트를 준비한다.42
이므로 setAge(42+1)
이며 React가 '다음 렌더링'에 새로운 state의 값으로 업데이트를 준비한다.42
이므로 setAge(42+1)
이며 React가 '다음 렌더링'에 새로운 state의 값으로 업데이트를 준비한다.그래서 이전 상태를 기반으로 업데이트해야하는 경우라면 set 함수의 콜백 함수를 인자로 해서 사용하면 이전 상태를 기반으로 상태를 업데이트할 수 있다.
위의 코드를 이렇게 고치면 handleClick
함수 호출 시 3
이 더해진다.
function handleClick() {
setAge(prevState => prevState + 1); // setAge(42 => 42 + 1)
setAge(prevState => prevState + 1); // setAge(43 => 43 + 1)
setAge(prevState => prevState + 1); // setAge(44 => 44 + 1)
}
set 함수를 호출해서 state를 업데이트했는데 로그를 출력해보면 왜 여전히 이전의 값만 출력이 될까?
내가 React에서 useState를 사용해보면서 가장 많이 겪었던 문제였다.
아래의 코드는 Docs의 예제인데 결과를 자세히 보면 setCount
를 실행한 이후에도 로그에는 0이 찍히고 있다.
function handleClick() {
console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!
setTimeout(() => {
console.log(count); // Also 0!
}, 5000);
}
이것은 상태가 snapshot처럼 작동하기 때문이다. 상태를 업데이트하면 새로운 상태 값으로 렌더링이 요청이 되지만 이미 실행 중인 이벤트 핸들러의 JavaScript 변수인 count
에는 영향을 주지 못하기 때문이다.
만약 해당 함수에서 다음 상태의 값이 필요하다면 set 함수에 전달하기 전에 JavaScript 변수에 저장하면 된다.
function handleClick() {
const nextCount = count + 1;
setCount(nextCount);
console.log(count); // 0
console.log(nexCount); // 1
}