Search

상태(state) 관리

프론트엔드에서 상태(state)는 반응적(reactive)인 데이터입니다. 사용자의 UI 상호작용이나 특정 이벤트에 의해 값이 변경되는 데이터를 말하는 것이죠. 그리고 변경된 데이터를 즉각적으로 애플리케이션에 반영해 화면을 갱신하는 것을 상태 관리(state management)라고 합니다.
상태 관리의 과정을 간단하게 나타내자면 위와 같습니다. 사용자가 웹 화면에서 요소를 클릭하는 것과 같은 상호작용을 하게 되면 브라우저에서 이벤트(event)가 발생합니다. 이때 발생한 이벤트에 의해 상태(state)가 변경된다면 해당 상태를 사용하는 컴포넌트는 랜더링을 다시 수행하여 변경된 상태를 화면에 반영합니다.
이 같은 리액트의 상태(state)를 잘 활용하면, 바닐라 자바스크립트나 JQuery를 사용할 때처럼 DOM API를 통해 화면 요소를 직접 수정하는 로직을 신경 쓰지 않아도 되기 때문에 보다 쉽게 반응적인 웹 애플리케이션을 만들 수 있습니다.

단방향 데이터 흐름

리액트에서 데이터는 한쪽 방향으로만 흘러가는 단방향 데이터 흐름(one way data flow)입니다. 데이터는 상위 컴포넌트에서 하위 컴포넌트로 하향식으로만 이동합니다. 이전 포스팅인 컴포넌트(component)와 props 에서 props는 상위 컴포넌트에서 하위 컴포넌트로만 지정해 줄 수 있다고 설명했었죠? 이는 props가 리액트의 하향식 단방향 데이터 흐름을 따르기 때문입니다.
이벤트(event)의 경우는 방향이 반대입니다. 아래에서 위로 흘러가는 상향식 흐름입니다. 하위 컴포넌트에서 발생해서 상위 컴포넌트로 이어집니다. 이유는 DOM의 이벤트 버블링을 생각해보면 됩니다.
상태(state)는 컴포넌트 스스로 관리하는 지역(local)데이터입니다. 상태는 반응적이므로 값을 변경하려고 하면 컴포넌트는 값이 바뀌는 것을 감지하고 랜더링을 다시 수행합니다. 이를 통해 화면에는 state의 변경이 즉각적으로 반영되게 됩니다.
종합적으로 보면, 아래 이미지처럼 정리할 수 있습니다.
리액트는 데이터의 수월한 관리와 충돌이 발생하는 것을 방지하기 위해 단방향 데이터 흐름을 사용합니다.

상태(state)의 생성과 상태 변경 함수(set state)

리액트에서 상태를 생성하는 방법은 여러가지가 있지만 가장 쉽고 보편적으로 사용되는 방법은 리액트 Hook의 useState() 함수를 사용하는 방법입니다. useState()를 사용한 리액트 코드를 통해 상태의 생성과 변경에 대해 알아봅시다.
버튼을 누를때마다 화면의 값이 1씩 증가하는 간단한 카운터 컴포넌트를 만든다고 가정하겠습니다. 화면 요소로는 현재 카운트를 보여주는 <h1> 요소와 클릭할 때마다 카운트를 1씩 증가시키는 <button> 요소가 있습니다.
import ReactDOM from 'react-dom/client'; // Counter 컴포넌트 const Counter = () => { let count = 0; return ( <> <h1>현재 카운트: {count}</h1> {/* 클릭할 때마다 count 값에 1씩 더하기 */} <button onClick={() => count++}> + </button> </> ); }; // Counter 컴포넌트 랜더 const rootNode = document.getElementById('root'); ReactDOM.createRoot(rootNode).render(<Counter />);
JavaScript
복사
현재 카운트를 나타내는 count 변수는 state가 아니기 때문에 화면의 +버튼을 아무리 클릭해도 화면에 증가된 count가 나타나지 않습니다.
count 변수를 상태(state)로 만들기 위해 useState()를 사용하겠습니다.
useState()는 리액트 함수이므로 react 라이브러리에서 임포트합니다.
useState()의 첫번째 인자로 값을 지정하면 반환하는 상태의 초기 값이 됩니다.
useState()는 두 값을 가진 배열(Array)을 리턴하는데 첫번째 값이 초기화된 상태이고 두번째 값은 상태를 변경할때 사용되는 상태의 set 함수입니다. set 함수의 첫번째 인자로 변경할 값을 지정해 함수를 호출하면 상태가 변경됩니다.
useState()의 반환 값에 구조분해할당을 적용해 각각 명칭을 부여해 사용하는 것이 일반적인 방법입니다. 두번째 값인 상태 변경 함수를 이름 지을때는 앞에 “set”을 붙여서 변경 함수라는 것을 명시해 줍시다. 예를 들어 상태 명칭이 “count”라면 변경 함수 명칭은 “setCount”라고 이름 짓는 식입니다.
그럼 useState()로 상태와 함께 생성한 setCount() 상태 변경 함수를 사용해서 +버튼을 클릭했을때 count 상태를 1씩 올리는 함수를 아래처럼 만들 수 있습니다.
const handleClick = () => { setCount(count + 1); };
JavaScript
복사
해당 예에서는 위 코드처럼 작성해도 별다른 문제가 발생하지는 않지만, 기존 상태 값을 연산해서 새로운 상태 값으로 변경하는 처리를 할때는 인자에 직접 처리하는 것보다 함수 형태로 처리하는 것이 올바른 방법입니다. 상태 변경 함수에 함수식을 입력하면 첫번째 인자가 현재 상태 값이 됩니다. 인자로 받은 현재 상태 값을 연산해서 반환하면 그 값으로 상태가 변경됩니다. 위의 handleClick() 함수 코드를 아래처럼 수정하겠습니다.
const handleClick = () => { setCount((currentCount) => { return currentCount + 1; }); };
JavaScript
복사
현재 상태를 연산해서 새로운 상태로 변경할 경우 함수 형태로 인자에 포함시켜야 합니다.
기존 상태 값을 이용해서 상태를 변경할때 함수 형태로 처리해야하는 자세한 이유가 궁금하다면 리액트 Hook 시리즈의 useState 편을 참고하세요.
마지막으로 <button> 요소의 onClick 이벤트가 발생했을때 호출하는 부분을 handleClick() 함수로 교체하면 상태(state) 적용이 완료됩니다.
import { useState } from 'react'; import ReactDOM from 'react-dom/client'; // Counter 컴포넌트 const Counter = () => { const [count, setCount] = useState(0); const handleClick = () => { setCount((currentCount) => { return currentCount + 1; }); }; return ( <> <h1>현재 카운트: {count}</h1> <button onClick={handleClick}> + </button> </> ); }; // Counter 컴포넌트 랜더 const rootNode = document.getElementById('root'); ReactDOM.createRoot(rootNode).render(<Counter />);
JavaScript
복사
이제 +버튼을 클릭하면 화면의 count 값도 자동으로 올라가는 것을 볼 수 있습니다!

하위 컴포넌트에서 상태 변경

컴포넌트에서 생성한 상태(state)는 지역적이므로 기본적으로는 컴포넌트 함수 내 범위에서만 상태 값을 읽고 수정하는 것이 가능합니다만, 하위 컴포넌트에게 props를 통해 상태 또는 변경 함수를 내려주면 하위 컴포넌트에서도 상위 컴포넌트의 상태를 읽거나 수정하는 것이 가능해집니다.
이를 이해하기 위해 먼저 만들었던 <Counter /> 컴포넌트에서 버튼 요소를 별도의 <CountButton /> 컴포넌트로 분리하고, 하위 컴포넌트로 포함시킨 다음 props로 count 상태를 변경하는 함수를 넘겨 하위 컴포넌트에서 상위 컴포넌트의 상태를 수정하는 구조로 코드를 수정해보겠습니다.
거기에 더해서 이번엔 버튼을 두개를 생성해 하나는 카운트를 1씩 증가시키는 +버튼, 하나는 카운트를 1씩 감소시키는 -버튼으로 기능을 추가해보도록 하죠.
import { useState } from 'react'; import ReactDOM from 'react-dom/client'; // CountButton 컴포넌트 const CountButton = ({ symbol, onClick }) => { return ( <button onClick={onClick} > {symbol} </button> ); }; // Counter 컴포넌트 const Counter = () => { const [count, setCount] = useState(0); // count 상태를 1씩 증가시키는 함수 const handlePlus = () => { setCount((currentCount) => { return currentCount + 1; }); }; // count 상태를 1씩 감소시키는 함수 const handleMinus = () => { setCount((currentCount) => { return currentCount - 1; }); }; return ( <> <h1>현재 카운트: {count}</h1> <CountButton symbol="+" onClick={handlePlus} /> <CountButton symbol="-" onClick={handleMinus} /> </> ); }; // Counter 컴포넌트 랜더 const rootNode = document.getElementById('root'); ReactDOM.createRoot(rootNode).render(<Counter />);
JavaScript
복사
잘 작동 하는군요?
본 포스팅은 상태 관리에 대해 이해하는 것이 목적이므로 useState() 함수만 간단하게 알아봤습니다. 함수형 컴포넌트의 상태 관리에 사용되는 리액트 Hook은 useState() 외에도 useEffect(), useContext() 등 여러가지가 있습니다. 리액트 Hook 전체에 대한 자세한 설명은 React Hooks 시리즈에서 다루고 있습니다.
리액트 Hook을 사용해서 상태 관리를 하는 방법 외에 클래스 컴포넌트의 상태를 사용하는 방법도 있지만 권장되지 않으므로 제 블로그에서 다루지 않습니다. Redux, MobX와 같은 글로벌 상태 관리 라이브러리를 사용하는 것도 방법이지만 그 전에 리액트의 자체적인 상태 관리를 이해하는 것이 중요하기 때문에 본문에서 다루지 않았습니다. 글로벌 상태 관리에 대해서는 페이스북(현 메타)에서 개발한 Recoil 상태 관리 라이브러리 시리즈로 다룰 예정입니다.

 컴포넌트(component)와 props

 리액트 To Do List 앱 만들기

React 시작하기 목록