상태(state)란 "컴포넌트 내부에서 변경되는 데이터"를 의미합니다. 사용자와 상호작용하여 변경되는 데이터들을 상태로서 관리하면 상태가 변경될 때마다 React가 해당 컴포넌트를 재평가하여 변경된 상태값으로 UI를 화면에 리렌더링을 합니다.
state를 사용하는 이유는 UI와 state를 연동시키기 위해서 사용합니다. 근본적으로 UI는 어떤 데이터를 표시하는 것입니다. 리액트는 UI와 연동되어야 하고, 변할 여지가 있는 데이터를 state라는 형태로 사용할 수 있도록 도와줍니다. 이러한 state가 변경되면 UI가 그에 맞게 변화하기 위해서 state를 변경시키는 방법을 제한(setState)하고 해당 함수가 호출될 때마다 리렌더링되도록 설계되어 있습니다.
이러한 이유로 인해서 리액트에서 리렌더링이 발생하는 시점은 state가 변경되었을 때입니다. 특정 컴포넌트의 state가 변경되는 경우 해당 컴포넌트와 해당 컴포넌트의 모든 하위 컴포넌트들은 리렌더링이 발생합니다.
우리는 리액트로 "선언적 접근법(Declarative Approach)" 을 따릅니다. 선언적 접근법이란 상호작용이 많은 사용자 인터페이스를 만들때 생기는 어려움을 줄여줍니다. 어플리케이션의 상태에 대해서 우리는 "View만 설계"하면 리액트는 데이터(state)가 변경됨에 따라 적절하게 컴포넌트만 효율적으로 갱신하고 렌더링합니다.
const CustomComponent = () => {
let title = 'title';
const clickHandler = () => {
title = 'Updated!';
};
return (
<div>
<h2>{title}</h2>
<button onClick={clickHandler}>Click!</button>
</div>
);
};
위 예제에서 버튼을 클릭하면 h2 요소의 Content 영역 "title"이 "Updated!" 문자열로 바뀔 것이라고 예상했지만, 실제로는 변하지 않습니다.
함수 컴포넌트는 자바스크립트 함수입니다. 이 함수의 특별한 점은 리액트 엘리먼트를 반환한다는 점입니다. 반환된 리액트 엘리먼트가 실제 돔에 반영되어 브라우저에 렌더링됩니다. 이러한 작업은 초기에 "딱 한 번만 실행"됩니다.
여기서 알 수 있는 점은 우리가 title 변수의 값을 "Updated!"라는 문자열로 재할당한 뒤 갱신된 상태값으로 컴포넌트 함수를 재평가하지 않았습니다. title 변수의 값이 변경이 되더라도 컴포넌트는 다시 실행되지 않으며, 컴포넌트가 다시 실행된다고 하더라도 title 변수의 선언문이 새로 실행되어 초기 상태인 "title" 문자열을 갖게 될 것입니다.
그러므로 우리는 리액트가 컴포넌트의 "상태 변경을 감지"하고, "변경된 상태값으로 컴포넌트 함수를 재평가"하여 새로운 리액트 엘리먼트를 생성해야 합니다. 이러한 동작을 위해서 "react 라이브러리의 useState
훅"을 사용할 수 있습니다.
컴포넌트 함수의 경우에는 useState()
라는 리액트 훅을 사용하여 컴포넌트에 "상태를 추가"할 수 있습니다. useState
훅은 컴포넌트 함수 내부에서만 호출할 수 있는 함수이며, 여러 번 호출이 가능합니다.
useState
훅은 인수로 "초기 상태값"을 전달하면서 호출합니다. 호출이되면 useState
훅은 배열을 반환하는데 이때 반환된 배열의 첫 번째 요소는 언제나 "최신 상태값"을 갖고 있고, 두 번째 요소는 상태를 업데이트하는 "상태값 변경 함수"가 존재합니다.
import React, { useState } from 'react';
const Component = props => {
const [상태변수, 상태변경함수] = useState(초기값);
,,,
};
일반적으로 useState
훅은 배열을 반환하므로 배열 디스트럭처링 문법을 사용하여 변수를 선언하는 방법을 자주 사용합니다.
상태값을 변경하고 싶다면 useState
훅의 반환값인 배열의 두 번째 요소인 상태 변경 함수를 호출하여 상태를 변경할 수 있습니다. 이때 상태 변경 함수의 인수로 변경할 상태값을 인수로 전달하면서 호출합니다.
이후 상태 변경 함수에게 인수로 전달된 값(변경될 값)과 이전 상태값을 "서로 단순 비교(===
연산자)"하여 일치하지 않는 경우 상태 변경 함수의 인수로 전달된 값으로 상태값을 "대체(replace)"합니다.
상태값이 변경되었다면 상태값이 변경된 컴포넌트 함수가 "다시 호출(재평가)"됩니다. 다시 호출될 때 컴포넌트의 useState 함수도 다시 실행되는데 이때 상태값이 초기값으로 초기화를 진행한 경험이 존재한다면 초기 상태값이 아닌 리액트에게 "최신 상태 값"을 받아 최신 상태값을 갖는 배열이 반환됩니다.
즉, 상태값을 할당받는 변수에 값을 재할당하는 방식으로 상태값을 변경하는 것이 아니라 상태 변경 함수를 호출하는 방법으로 상태값을 변경해야 실질적인 상태값이 변경되고, 변경된 상태값으로 컴포넌트를 재평가하고 리렌더링이 됩니다.
컴포넌트 함수가 재평가되면 항상 그 상태의 "새로운 스냅숏(최신 상태)"을 얻게 됩니다.
const CustomComponent = () => {
const [title, setTitle] = useState('title');
const clickHandler = () => {
setTitle('Updated!'); // 상태 변경 함수 호출
};
return (
<div>
<h2>{title}</h2>
<button onClick={clickHandler}>Click!</button>
</div>
);
};
이제 버튼을 클릭하면 clickHandler 이벤트 핸들러가 호출되어 setTitle 상태 변경 함수에 "Updated!" 문자열이 인수로 전달되면서 호출됩니다. 그러면 리액트는 CustomComponent 컴포넌트를 다시 호출하여 재평가하는데 title 상태 변수에는 "title" 문자열이 아닌 최신 상태값인 "Updated!" 문자열이 할당되어 재평가됩니다.
객체 타입을 상태값으로 사용하는 경우 주의해야 할 점이 있습니다.
객체를 상태값으로 사용하는 경우, 상태 변경 함수를 통해 상태를 변경할 때 "객체의 모든 프로퍼티를 모두 작성"해야 합니다.
객체는 참조형 데이터 타입으로 객체 구조가 동일하달고 하더라도 그 값은 일치하지 않습니다. 이는 상태를 변경하는 상태 변경 함수에 인수로 객체를 전달하면 "언제나" 이전 상태값인 객체와 일치하지 않기 때문에 인수로 전달된 객체의 값으로 상태값이 "대체"되기 때문에 반드시 객체의 "모든 프로퍼티를 작성"해주어야 합니다.
상태 변경 함수의 경우 항상 "비동기"로 동작합니다. 즉, 상태를 변경한다고 해서 바로 새로운 상태값으로 반영되지 않습니다.
import React, { useSate } from 'react';
const MyComponent = () => {
const [value, setValue] = useState(0);
const valueChangeHandler = () => {
setValue(value + 1);
console.log(value);
};
return (
<>
<div>{value}</div>
<button onClick={valueChangeHandler}>click</button>
</>
);
};
위 코드에서 버튼을 클릭하면 valueChangeHandler
이벤트 핸들러가 호출됩니다. 그리고 setValue
가 호출되어 상태를 업데이트한 이후에 다음 코드에서 console.log
로 value
상태값을 확인하면 결과는 1이 아닌 0으로 출력되는 것을 확인할 수 있습니다.
즉, 상태 변경 함수가 비동기 함수로 동작한다는 것을 확인할 수 있습니다.
또한 상태 변경 함수를 짧은 시간 내 여러 번 호출한다고 해서 상태 변경 함수를 호출한 횟수만큼 상태를 변경된다는 보장도 없습니다. 리액트는 상태를 변경하는 함수를 여러 변 호출한다고 해도 최소한의 변경을 하기 위해 "상태 변경 함수 호출 정보를 모았다가 한 번에 업데이트"합니다.
이러한 방식을 "batch update"
라고 합니다. batch update는 16ms 단위로 진행됩니다. 16ms 동안 발생된 상태 변경 함수 호출 정보를 모아서 한 번에 업데이트를 진행하고, 리렌더링도 한 번만합니다. 이러한 행동은 웹 페이지 랜더링 횟수를 줄여 좀 더 빠른 속도로 동작하게끔 만들어 줍니다.
비동기로 업데이트되는 상태값 때문에 이전 상태값에 의존하여 상태값을 변경하는 경우에는 아래와 같이 상태 변경 함수의 인수로 콜백 함수를 전달하는 방법을 사용하여 상태값을 변경해야 합니다.
상태 변경 함수의 경우 두 가지 방법으로 상태를 업데이트할 수 있습니다.
상태값 전달
첫 번째 방법은 앞에서 설명한 것처럼 상태 변경 함수의 인수로 "새로운 상태값"을 전달합니다. 이때 상태는 비동기로 업데이트가 되며 바로 상태에 접근하면 잠재적 문제가 발생할 수 있고 예상치못한 결과를 얻을 수 있습니다.
콜백함수 전달
두 번째 방법으로 상태 변경 함수의 인수로 "콜백함수"를 전달하는 것입니다. 만약 이전 상태에 의존하여 상태가 업데이트가 된다면 이 방법을 권장합니다.
상태 변경 함수의 인수로 전달한 콜백함수는 인수로 이전 상태값, 즉 가장 최근 상태값을 인수로 전달받습니다. 그리고 새로운 상태값을 반환값에 작성하면 반환값으로 상태가 업데이트됩니다.
이는 상태 변경 함수가 비동기로 동작하기 때문에 상태 변경 함수를 여러 번 동시에 호출한다면, 유효하지 않은 혹은 부정확한 이전 상태값에 의존하게 될 것입니다.
콜백함수의 인수로 전달되는 상태값은 "가장 최근 상태값"이라는 것이 보장됩니다. 즉, 가장 안전하게 상태를 업데이트하는 방법입니다.
const [state, setState] = useState(0);
setState(prevState => (prevState + 1));
위 코드에서 preValue 매개변수에는 가장 최근의 value 상태값을 인수로 전달받습니다. 그리고 반환값으로 value 상태를 업데이트합니다.
preValue에는 가장 최신의 상태값이 전달되는 것이 보장됩니다.
상태가 업데이트되어 컴포넌트가 재평가될 때 모든 변수는 초기화되어 실행될 것입니다. 하지만 상태는 어떻게 이전 상태를 기억하는지는 아래에 설명으로 얘기하겠습니다.
함수 컴포넌트에서 상태를 업데이트하는 순서는 다음과 같습니다.
컴포넌트가 처음 생성이 되면 useState
훅의 인수로 전달한 값으로 초기 상태값을 설정합니다.
이후 상태 변경 함수에 새로운 상태값을 전달하면서 호출하면 이전 상태값과 인수로 전달받은 값을 서로 단순 비교(=== 연산자)하여 일치하지 않은 경우에만 인수로 전달한 값을 리액트에게 전달하고 리액트는 상태값을 대체합니다.
함수 컴포넌트가 재평가되어 useState
훅을 다시 호출될 때 리액트에게 최신 상태값을 전달받아 상태 변수에 할당한 배열을 전달받고, 갱신된 상태값으로 컴포넌트 함수를 실행하여 return 문에 작성된 리액트 엘리먼트를 평가하여 화면에 UI를 리렌더링합니다.
즉, useState
훅이 반환하는 배열에는 "가장 최근의 상태값"을 리액트가 제공하게 됩니다. 리액트는 언제나 함수 컴포넌트가 재평가될 때마다 그 상태의 스냅숏을 리액트가 기억하여 전달해줍니다.
useState
훅에 전달한 인수는 초기값을 설정할 뿐입니다. 즉, 컴포넌트가 처음 만들어질 때의 상태값을 의미하며 상태가 변경되어 컴포넌트가 재평가될 때 상태값을 useState
훅의 인수로 전달한 초기값으로 초기화하지 않고 대신에 이 상태가 과거에 "초기화가 되었는지를 탐지"할 것이며, 이전에 초기화가 된 상태라면 "가장 최근의 상태값"을 전달해줄 것입니다.
상태값을 할당받는 상태 변수는 불변 변수는 아닙니다. 하지만 상태 변수도 "불변 변수"처럼 사용해야 합니다.
불변 변수란 "변수의 값을 변경할 수 없는 변수"
를 말합니다.
이전에 살펴본 props는 불변 객체로 때문에 값을 변경하려고 시도하면 에러가 발생합니다. 하위 컴포넌트에게 전달되는 props는 상위 컴포넌트에서 관리되는 값이므로 수정할수 없도록 막혀있습니다.
만약 props를 변경하고 싶다면 이 값을 관리하고 있는 "상위 컴포넌트에서 직접 변경"하여 다시 하위 컴포넌트에게 props로 전달해야합니다.
상태 변수 또한 불변 변수로 관리해야 하는 이유는 실제 상태값은 리액트에서 관리하고 우리는 단지 리액트가 관리하는 상태값을 전달받아 상태 변수에 할당할 뿐입니다.
때문에 아무리 우리가 상태 변수에 값을 재할당하더라도 실질적인 상태값은 변경되지 않으며, 컴포넌트가 재평가되어 리렌더링되지도 않습니다.
즉, 상태 변수에 값을 재할당하여 변경하는 것은 어떤 의미도 없는 동작이며 컴포넌트에게 아무런 영향도 주지 못합니다.
상태에 대한 업데이트는 여기에서 추가적으로 설명하겠습니다.