변수를 setState로 수정했는데 업데이트 된 값이 나타나지 않는 문제
예를 들어, 다음과 같이 투두 앱을 만들 때 할일 목록에 할일 추가하는 버튼을 만드는 상황이라고 해보자.
import { useState } from 'react';
export default function App() {
const [todos, setTodos] = useState(['할일 1', '할일 2']);
const addTodo = () => {
let newTodos = todos;
newTodos.push('새로운 할일');
setTodos(newTodos);
console.log(newTodos);
};
return (
<div>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
<button onClick={addTodo}>할일 추가</button>
</div>
);
}
위 코드에서는 아무리 할 일 추가 버튼을 눌러도, 새로운 할일이 화면에 나타나지 않는다. 그런데, 로그에 있는 `newTodos` 변수에서는 '새로운 할일' 추가가 잘 된다.
이는 자바스크립트의 `[ ]` 가 `primitive`가 아닌 `object` 기반이기 때문이다.
Primitive vs. Object 타입
자바스크립트에는 7가지 primitive 타입이 있다. 자바스크립트에서 primitive는 object가 아니며 method나 property가 없는 데이터를 말한다.
- `string`
- `number`
- `bigint`
- `boolean`
- `undefined`
- `symbol`
- `null`
(https://developer.mozilla.org/en-US/docs/Glossary/Primitive)
그러나 리액트에서처럼 `[ ]`를 이용해서 만들어진 변수는 자바스크립트의 `Array`라는 타입을 가진다. Array는 object를 기반으로 하는 데이터 타입으로, primitive와는 다르다.
console.log(typeof []); // "object"
console.log(typeof {}); // "object"
console.log(typeof "hello"); // "string" (primitive)
console.log(typeof 42); // "number" (primitive)
두 데이터 타입의 가장 큰 특징은 변수에 할당 시, 무엇을 바라보는지이다. number, string, boolean, null 같은 타입들은 값 자체를 바라보고, Array 등과 같은 object 기반 타입들은 메모리주소를 바라본다. 따라서 다음과 같은 차이가 생긴다.
let a = 5;
let b = 5;
console.log(a === b); // true - 같은 값
let arr1 = [1, 2, 3];
let arr2 = [1, 2, 3];
console.log(arr1 === arr2); // false - 서로 다른 메모리 주소
let arr3 = arr1;
console.log(arr1 === arr3); // true - 같은 메모리 주소를 참조
리액트 상태 변경 시 변수비교
리액트는 어떤 변수의 상태가 변경되었을 때, 자원 절약을 위해 내부적으로 이전 상태와 비교하여 변수가 변했는지 아닌지를 판단한다. 이때, 리액트에서는 얕은 비교를 통해 상태의 변경여부를 판단한다.
const addTodo = () => {
let newTodos = todos;
newTodos.push('새로운 할일');
setTodos(newTodos);
console.log(newTodos);
};
따라서 위에서, `newTodos`에 새로운 값을 넣고 이를 다시 `setTodos()` 함수를 통해 상태를 변경하면, 리액트 내부적으로는 다음과 같은 단계를 밟게 된다.
- 이전 state: `todos` (메모리 주소 A 참조)
- 새로운 state: `newTodos` (여전히 메모리 주소 A 참조)
- 같은 메모리 주소를 바라보고 있으므로, 변경 없다고 판단하여 재렌더링 진행 X.
때문에 `newTodos` 끝에는 새로운 할일이 추가되었는데 화면에는 나타나지 않는 상황이 발생한다.
해결 방법
문제가 발생한 이유가 같은 메모리 주소를 바라보기 때문이므로, 스프레드 연산자(`...`)를 이용하여 새로운 배열을 생성하여 해결한다.
const addTodo = () => {
let newTodos = [...todos];
newTodos.push('새로운 할일');
setTodos(newTodos);
};
짧게 쓰면, 아래와 같이 쓸 수 있다.
const addTodo = () => {
setTodos([...todos, '새로운 할일');
};