리액트 state를 처음 공부하며 왜 state를 쓰는지 궁금해서 일주일간 열심히 알아보았고 그동안 알게 된 것들을 포스팅해 보려고 한다.
state는 간단하게 말해서 변수이다. 하지만 const, let 등으로 선언한 변수와 다르게 값이 변하면 관련 있는 컴포넌트들이 재렌더링되어 화면이 바뀐다.
state는 컴포넌트의 내부에서 변경 가능한 데이터를 다루기 위해 사용하는 객체이다.
일반적으로 리액트에서는 유동적인 데이터는 변수에 담아서 사용하지 않고 useState()라는 리액트 함수를 사용하여 state라는 저장 공간에 담아 사용한다.
import { useState } from 'react';
const [state, setState] = useState(초기값(생략 가능));
여기서 state는 현재 상태값, setState는 state를 업데이트하는 함수를 의미한다.
보통 업데이트하는 함수는 앞에 set을 붙여 카멜케이스로 쓴다. 예를 들면 const [title, setTitle] = useState('');
와 같이 선언할 수 있다.
* 카멜케이스(camelCase)란? 중간 글자들은 대문자로 시작하지만 첫 글자는 소문자로 작성하는 것
let이 아니라 const로 선언하는 이유는?
👉 변수의 재할당을 막기 위해서
👉🏻 let을 사용하면 state=100과 같은 변수 형식의 할당이 가능해지기 때문에 이를 방지하고 setState를 사용한 변수 변경만을 허락하기 위해서 const로 선언한다.
useState는 함수 안에서 직접적으로 선언되어야 한다. 예를 들면 아래와 같다. 하지만 예외가 있다고 들었는데 알게 되면 수정할 예정이다.
const ExpenseItem = (props) ⇒ {
이곳에 있어야 한다.
const expenseData = {
여기에 있으면 안 된다.
}
이제 정말로 리액트에서 state를 사용하는 이유를 알아보자.
변수는 변경되어도 자동으로 화면이 바뀌지 않는다.
하지만 state는 변경되면 자동으로 화면이 바뀌기 때문에 state를 사용한다.
즉 유동적인 변수를 사용할 때 화면에 그려지는 변수도 정상적으로 변경되길 원한다면 사용한다.
이게 무슨 말인지 아래에서 숫자를 세는 Counter 예시로 알아보자.
일반 변수 count를 선언하고 h2 태그 안에 count 변수를 썼다.
그리고 +
버튼을 누르면 plus라는 함수가 실행되고, -
버튼을 누르면 minus 함수가 실행되게 했다.
plus 함수는 count 변수를 count보다 1 큰 수로 변경한다.
그리고 minus 함수는 count 변수를 count보다 1 작은 수로 변경한다.
그래서 함수가 실행됐을 때, count 변수가 1씩 커지거나 작아지므로 당연히 count 변수가 있는 자리의 화면도 변경되는 것을 기대했다.
const Counter = () => {
let count = 0;
const plus = () => {
count = count + 1
console.log(count); // 제대로 증가함
}
const minus = () => {
count = count - 1
console.log(count); // 제대로 감소함
}
return (
<div className='container'>
<h2 className='int'>{ count }</h2>
<button className='plus' onClick={plus}>+</button>
<button className='minus' onClick={minus}>-</button>
</div>
)
}
export default Counter;
하지만 아래의 영상을 보면 알 수 있듯이 화면은 변경되지 않는다.
그렇다고 count가 변경되지 않은 것은 아니다. plus 함수와 minus 함수를 실행한 후에 콘솔로 count 값을 출력해 보면 count 값은 정상적으로 변경됨을 알 수 있다.
그렇다면 화면이 변경되지 않은 이유는 무엇일까?
화면이 변경되지 않은 이유는 일반 변수를 사용했기 때문이다. 일반 변수는 변경되어도 자동으로 화면이 재렌더링되지 않는다.
하지만 state는 다르다. 리액티브한 프론트엔드에서 상태는 단순한 변수가 아니라 이 값이 변했을 때 화면에 반영되도록 연결된 것을 상태라고 한다. 그래서 상태가 아닌 일반 변수는 바뀌어도 화면이 변하지 않는 것이다.
아래에서 일반 변수 대신 state를 사용한 Counter 예시를 살펴보자.
일반 변수 대신 state를 사용하여 Counter 예제를 구현해 보았는데,
함수가 실행될 때마다 setCount로 기존의 count 값을 count+1 혹은 count-1로 변경해 주었다.
import { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
console.log(count);
const plus = () => {
setCount(count + 1);
}
const minus = () => {
setCount(count - 1);
}
return (
<div className='container'>
<h2 className='int'>{ count }</h2>
<button className='plus' onClick={plus}>+</button>
<button className='minus' onClick={minus}>-</button>
</div>
)
}
export default Counter;
count 변수가 변할 때마다 이에 맞춰 화면도 정상적으로 변하는 것을 확인할 수 있다!
위의 예제를 통해 일반 변수가 아닌 state를 사용하면 변수 값이 변경되었을 때 화면이 의도대로 재렌더됨을 알 수 있었다.
이제까지 리액트에서 state를 사용하는 이유를 알아보았다.
아래에서는 추가적으로 setState가 동작하는 방식과 왜 그렇게 동작하는지를 알아보자.
setState는 비동기적으로 동작하는데, setState 바로 아래에 console.log로 count를 출력해 봤을 때 확인할 수 있다.
import { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const plus = () => {
setCount(count + 1);
console.log(count); // setCount로 count를 변경한 후 바로 콘솔에 찍었다
}
const minus = () => {
setCount(count - 1);
console.log(count); // setCount로 count를 변경한 후 바로 콘솔에 찍었다
}
return (
<div className='container'>
<h2 className='int'>{ count }</h2>
<button className='plus' onClick={plus}>+</button>
<button className='minus' onClick={minus}>-</button>
</div>
)
}
export default Counter;
분명히 setCount로 count를 변경했는데, 변경한 후에 console.log로 찍어보니 값이 바로 바뀌지 않는다.
그 이유는 setState가 비동기이기 때문이다. (아래의 동기/비동기 설명 출처)
setCount는 이벤트 핸들러 안에서 현재 state의 값에 대한 변화를 요청하기만 하는 것이라서 이벤트 핸들러가 끝나고 리액트가 상태를 바꾸고 화면을 다시 그리기를 기다려야 한다.
또, 리액트는 이벤트 핸들러가 닫히는 시점에 setState를 종합하여 한 번에 처리한다. state도 결국 객체이기 때문에, 같은 키값을 가진 경우라면 가장 마지막 실행값으로 덮어씌워지는데 이는 객체를 합치는 함수인 Object.assign()
에서 확인할 수 있다.
아래처럼 plus 함수 안에 setCount를 세 번 썼을 때, 1+2+3으로 6씩 증가하는 것이 아니라 마지막 setCount의 결과인 3씩 증가하게 된다.
const plus = () => {
setCount(count + 1);
console.log(count);
setCount(count + 2);
console.log(count);
setCount(count + 3);
console.log(count);
}
만약 6씩 더하고 싶은 거라면, 이 문제는 이렇게 하면 해결된다.
const plus = () => {
setCount(count => count + 1);
setCount(count => count + 2);
setCount(count => count + 3);
}
setCount(count+1)
에서 count는 렌더링 시작 시점의 count이기 때문에 count가 최근에 바뀌었어도 반영되지 않는다. 하지만 이렇게 콜백 함수를 사용하면 항상 최신의 값을 인자로 받아와서 처리하기 때문에 setCount(count => count + 1)
를 쓰면 최신 값을 받아서 처리할 수 있다.리액트 공식문서: Updating the same state multiple times before the next render 참고
state는 값이 변경되면 리렌더링이 발생하는데, 변경되는 state가 많을수록 리렌더링이 계속 일어나고 속도도 저하되는 등 성능적으로 문제가 많을 것이다.
그래서 16ms 동안 변경된 상태 값들을 모아서 한 번에 리렌더링을 진행하는데 이를 batch(일괄) update라고 한다.
바닐라 자바스크립트로도 useState()와 비슷한 기능을 구현할 수 있다.
이번에도 Counter 예시이다. 순수 자바스크립트로 구현되었다는 점만 위와 다르다.
아래의 코드에서는 변수를 변경하고 .textContent
로 직접 number가 있는 html을 count 값으로 바꿔줬다. number.textContent = count;
코드가 없었다면 화면의 count 변수는 변경되지 않았을 것이다.
<!--.html-->
<button id="plus">숫자증가</button>
<button id="minus">숫자감소</button>
<span>0</span>
const plusBtn = container.querySelector('#plus');
const minusBtn = container.querySelector('#minus');
const number = container.querySelector('span');
plusBtn.addEventListener('click',function(){
let count = Number(number.textContent)
count = count + 1;
number.textContent = count;
});
minusBtn.addEventListener('click',function(){
let count = Number(number.textContent)
count = count - 1;
number.textContent = count;
});
아래의 코드에서는 setCount라는 함수를 만들어서 리액트처럼 변수가 변경될 때마다 자동으로 화면이 재렌더링되도록 했다.
const plusBtn = container.querySelector('#plus');
const minusBtn = container.querySelector('#minus');
const number = container.querySelector('span');
let count = 0;
function setCount(newValue){
count = newValue;
number.textContent = count;
}
plusBtn.addEventListener('click',function(){
setCount(count + 1);
});
minusBtn.addEventListener('click',function(){
setCount(count - 1);
});