리액트 개발에서 State의 관리는 애플리케이션의 동적인 부분을 다루는 데 있어 핵심적인 역할을 한다. 이번 포스팅에서는 State의 기본 개념과 활용 방법, 특히 참조형 State를 다루는 데 주의해야 할 점들에 대해 자세히 살펴보자.
State는 리액트 컴포넌트의 '상태'를 의미한다. 상태가 변경될 때마다 리액트는 해당 컴포넌트를 다시 랜더링에 화면에 반영해준다.
리액트에서는 이를 통해 동적인 사용자 인터페이스를 구현하는 데 매우 중요하고 강력한 기능을 수행할 수 있게 된다.
리액트에서 State를 만들기 위해 useState 함수를 사용한다. 이 함수는 초기값을 인자로 받고, State 값과 이를 업데이트하는 함수를 배열로 반환한다.
일반적으로 리액트에서는 Destructuring을 사용해 이들을 추출한다.
아래 간단한 예제를 통해 State를 살펴보자. 여기서 첫 번째 요소인 num은 State값으로 현재 변수의 값을 나타낸다. 두 번째 요소인 setNum은 setter함수로, 이 함수를 호출할 때 파라미터로 전달하는 값으로 State값이 변경이 되게 된다.
import { useState } from 'react';
function App() {
const [num, setNum] = useState(1);
// ...
}
State를 변경할 때는 직접 변수에 값을 할당하는 대신, State를 업데이트하는 함수(setter 함수)를 사용한다. 이 함수에 새로운 값을 전달하면, 리액트는 해당 State를 업데이트하고 컴포넌트를 다시 렌더링하게 된다.
const handleRollClick = () => {
setNum(3); // num State를 3으로 변경
};
자바스크립트의 자료형은 크게 기본형(Primitive type)과 참조형(Reference type)로 나눌 수 있다는 사실을 지난 글을 통해 알아봤었다.
특히 참조형 값들은 조금 독특한 특성을 가지고 있어서 변수로 다룰 때도 조금 주의해야 할 부분들이 있다고 강조하였었는데, state를 활용할 때도 마찬가지이다.
자바스크립트의 참조형 데이터(예: 배열, 객체)를 State로 사용할 때는 특히나 더 주의가 필요하다. 참조형 데이터는 값이 아닌 메모리 주소를 참조하기 때문에, 데이터를 직접 수정하는 것은 State의 업데이트로 인식되지 않을 수 있기 때문이다.
아래 코드에서 gameHistory 배열에 직접 값을 추가했지만, 배열의 참조 주소는 변하지 않았기 때문에 리액트는 이를 새로운 State로 인식하지 않게 된다.
const [gameHistory, setGameHistory] = useState([]);
const handleRollClick = () => {
gameHistory.push(nextNum); // 잘못된 방식
setGameHistory(gameHistory);
};
참조형 State를 업데이트할 때는 항상 새로운 객체나 배열을 생성해야 한다. 즉, 전체를 새로 만드는 것이다. 그래야만이 참조형 데이터가 새로운 주소값을 가지게 되고, 이를 통해서 업데이트가 진행 될 수 있는 것이다.
가장 일반적인 방법은 Spread 문법을 사용하는 것이다. Spread문법을 사용하면 새로운 배열이 생성되고, 리액트는 이를 새로운 State로 인식하여 컴포넌트를 적절히 업데이트할 수 있게 된다.
const handleRollClick = () => {
const nextNum = random(6);
setGameHistory([...gameHistory, nextNum]); // 올바른 방식
};
비동기 작업을 수행할 때 useState의 상태 업데이트는 조금 더 주의가 필요하다. 만약 이전 State 값을 참조하면서 State를 변경하는 경우, 비동기 함수에서 State를 변경하게 되면 최신 값이 아닌 State 값을 참조하는 문제가 발생 가능하다. 즉, 잘못된 시점의 값을 사용하게 되는 문제가 발생 가능한 것이다.
이럴 때 위 문제를 setter함수의 값이 아니라 콜백을 전달해서 해결할 수 있는데, 파라미터로 올바른 State 값을 가져와서 사용하여 문제를 해결 가능하다. 이 때, 파라미터는 고정된 값이 아니라 함수의 파라미터값이기 때문에 react가 현재 시점의 state값을 전달해줄 수 있게 되는 것이다.
아래 코드는 setCount 함수 내부에서 콜백을 사용함으로써, 비동기 작업 후에도 정확한 이전 상태 값을 기반으로 상태를 업데이트한 예를 보여준다.
const [count, setCount] = useState(0);
const handleAddClick = async () => {
await someAsyncFunction();
setCount((prevCount) => prevCount + 1);
};
State의 초기값이 복잡한 계산이 필요한 경우, 콜백 함수를 통해 초기값을 지정할 수 있다. 이 방법은 초기 렌더링 시에만 콜백 함수가 호출되므로, 불필요한 계산을 방지할 수 있다는 장점을 가지고 있다.
const [values, setValues] = useState(() => {
const savedValues = getSavedValues(); // 복잡한 계산
return savedValues;
});
State는 리액트에서 동적인 데이터 관리의 핵심이며, State를 올바르게 사용하고 관리하는 것은 사용자에게 뛰어난 인터랙티브 경험을 제공하는 데 필수적이다.
이러한 이점 때문에, 나 또한 State를 매우 많이 사용하며 개발을 진행해왔었다. 하지만 이번에 State의 개념에 대해 학습해보며, 그동안 State를 올바르게 사용했는지에 대한 반성을 많이 하게 되었다.
또한, 그 동안 State를 사용할 때의 주의점들에 대해 심도깊게 숙지하고 있지는 못했었는데, 이번 학습을 통해 주의점들을 숙지할 수 있어 개인적으로 굉장히 의미깊었다고 생각된다. 특히 참조형 데이터를 State로 사용할 때는 새로운 데이터 구조를 생성하여 업데이트하는 것을 잊지말고 명심하자.