
상태란 결국 데이터이자 객체이다. 객체라고 한다면 어떤 속성을 가진 데이터를 저장하고 있을텐데 어떠한 데이터를 저장하고 있을까? 바로 컴포넌트가 현재 어떤 ui를 보여줄지에 대한 현재 상황이다.

상태가 단순히 데이터이자 객체라고 한다면 기존에 변수를 사용하면 되는 것 아닌가? 하는 생각이 든다. 그렇다면 단순히 값을 담고 있는 변수와 상태는 어떻게 다른것일까?
상태라는 개념이 등장하게 된 배경은 단순하다. 웹 시장이 커짐과 동시에 웹 어플리케이션 또한 단순히 정적 데이터만을 서버에서 다운받아 화면에 띄워주는 수준을 넘어섰고 이제는 사용자에게 입력값을 받아 그에 상응하는 최신의 데이터를 보다 빠르게 렌더링 해주는 것이 중요해졌다.
상태는 예를 들면 사용자에게 입력을 받는 input이 제출되었는지(isSubmit), 기능이 비활성화 되지는 않았는지(disabled), 모달 창이 열렸는지 닫혔는지(isOpen,isClosed), 서버에서 데이터를 받아오는 중인지 다 받아온 상태인지(isLoading) 와 같은 데이터들이다.
만약 사용자가 어플에서 특정 모달창을 보고 싶어 버튼을 클릭했는데 변수값이 그대로 isClosed라고 한다면 모달 창을 바로 보는 경험을 할 수 있을까? 그렇지 않을 것이다. 변수는 재할당이 필요하기 때문이다. 상태는 사용자의 상호작용에 따라서 즉각적으로 update되어야 하는 특성이 있다는 것이 일반 변수와의 중요한 차이점이다.
앞서 상태가 무엇인지, 그리고 그 필요성에 대해 알아보았다. 그렇다면 "상태 관리"란 무엇일까?
상태란 컴포넌트가 현재 어떤 UI를 보여줄지에 대한 상황을 설명하는 데이터라고 했다. 그런데 문제가 발생한다. 사용자의 상호작용 요소에 따라서 다르게 보여주어야 하는 ui가 너무 많아졌다는 것이다. 이는 개발의 효율성과 밀접한 관련이 있다.
왜일까? 1차적으로 상태도 정의하는 코드가 필요하다. 상태가 많아지면 동시에 코드의 가독성이 떨어지고 버그가 발생할 확률이 높아진다는 의미이다. 더구나 하나의 상태를 여러 컴포넌트가 공유해야 하는 상황이 생기면 어떨까? 이 때 만약 A 컴포넌트 따로, B 컴포넌트 따로 상태를 정의한다면 과연 데이터를 일관적으로 유지할 수 있을까? 과장해서 Z~9 컴포넌트까지 똑같은 상태를 공유한다면 모두 동시에 UI가 바뀔 수 있도록 상태를 잘 추적할 자신이 있는가?
이처럼 상태 관리를 얼마나 잘 관리하느냐는 사용자 경험에도 막대한 영향을 주는 요소로 자리 잡혔기 때문에 웹 개발에서 매우 중요한 개념이 되었다.

이처럼 웹 시장이 커짐과 동시에 상태 관리를 어떻게 하느냐가 중요한 요소로 논쟁이 되는 상황 가운데, 상태 관리를 보다 쉽고 효율적으로 할 수 있도록 도와주는 Redux와 Mobx와 같은 라이브러리가 등장하였다. Flux 패턴에서 영향을 많이 받은 Redux는 단방향 데이터 흐름을 기반으로 하여 상태를 중앙에서 관리할 수 있도록 도와주며, 예측 가능한 상태 관리를 가능하게 해준다. Mobx는 반응형 프로그래밍을 통해 상태와 UI의 연결을 보다 직관적으로 만들어주며, 필요한 부분만 업데이트하도록 해준다.
📍 Flux 패턴이란?
Flux 패턴은 구 페이스북에서 개발한 아키텍처로, 기존의 양방향 데이터 흐름을 단방향으로 변경하였다는 점이 특징이다. (Model: JavaScript → View: HTML) 이렇게 데이터 흐름이 명확해짐으로써 데이터의 흐름을 추적하기가 쉬워지는 이점을 제공해주었다.
2018년 리액트 16.8 버전에서 훅(Hook)이라는 개념이 도입되었다. 훅은 쉽게 말해 함수이지만, 일반 함수와의 차별점은 상태를 관리하고 부수 효과를 처리하는 로직이 포함되어 있다는 점이다. 훅의 등장으로 인해 기존에 복잡한 로직을 필요로 했던 클래스형 컴포넌트를 사용할 필요가 없어졌다. 예를 들어, 생성자(constructor), this 바인딩, 생명주기 메소드(mount, update, unmount 시 실행할 함수 등)를 따로 정의할 필요가 없게 되면서, 함수형 컴포넌트로의 패러다임 전환이 이루어졌다.
hook의 등장으로 Recoil, Jotai, Zustand와 같은 새로운 상태 관리 라이브러리들이 등장하였다. 이들은 hook 기능을 기반으로 설계되었기 때문에 상태 관리의 복잡성이 비교적 줄어들었고, 보다 직관적인 API를 제공한다. 이 중에 Recoil 하나만 살펴보겠다.

Recoil은 두 가지 주요 개념인 Atoms와 Selectors를 사용한다. Atoms는 상태의 최소 단위로, 여러 컴포넌트에서 공유될 수 있는 상태를 정의한다. Selectors는 파생 상태를 계산하는 데 사용되며, 다른 Atoms나 Selectors의 값을 기반으로 새로운 상태를 만들어낸다.
앞서 Recoil은 hook을 기반으로 했기 떄문에 직관적인 api를 제공한다고 했다. useRecoilValue, 그리고 useSetRecoilState 같은 커스텀 훅이 그 예시이다.
아래는 Recoil을 사용하여 단순히 카운트(x1, x2)기능을 하는 함수이다.
import React from 'react';
import { atom, selector, useRecoilState, RecoilRoot } from 'recoil';
// 1. Atom 정의 : countState라는 Atom을 정의하여 카운트 값을 관리합니다. 이 상태는 여러 컴포넌트에서 공유될 수 있습니다.
const countState = atom({
key: 'countState', // 고유한 ID (다른 Atom과 겹치지 않도록)
default: 0, // 기본값
});
// 2. Selector 정의:doubledCountState라는 Selector를 정의하여 countState의 두 배 값을 계산합니다. Selector는 다른 상태의 값을 기반으로 새로운 값을 생성하므로, 상태 계산을 쉽게 관리할 수 있습니다.
const doubledCountState = selector({
key: 'doubledCountState',
get: ({ get }) => {
const count = get(countState);
return count * 2; // count의 두 배를 반환
},
});
// 3. 카운터 컴포넌트:Counter 컴포넌트에서는 useRecoilState 훅을 사용하여 카운트 상태를 읽고 업데이트합니다. 버튼 클릭 시 카운트를 증가시키는 간단한 로직을 포함하고 있습니다.
const Counter = () => {
const [count, setCount] = useRecoilState(countState); // 상태 읽기 및 업데이트
return (
<div>
<h1>Count: {count}</h1>
<h2>Doubled Count: <Value /></h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
// 4. 두 배 카운트 값을 표시하는 컴포넌트:Value 컴포넌트에서는 useRecoilValue 훅을 사용하여 Selector의 값을 읽어와서 표시합니다. 이는 카운트를 두 배로 계산한 값을 간편하게 가져오는 방법입니다.
const Value = () => {
const doubledCount = useRecoilValue(doubledCountState); // Selector를 통해 값 읽기
return <span>{doubledCount}</span>;
};
// 5. RecoilRoot로 애플리케이션 감싸기:애플리케이션의 최상위 컴포넌트를 RecoilRoot로 감싸주어 Recoil의 상태 관리 기능을 사용할 수 있도록 설정합니다.
const App = () => (
<RecoilRoot>
<Counter />
</RecoilRoot>
);
export default App;
이처럼 Recoil은 hook을 사용하여 상태를 읽고 쓰는 것을 간편하게 만들어주며, Atoms와 Selectors를 사용하여 상태 관리의 복잡성을 줄여주었고. 이로 인해 개발자는 더욱 직관적으로 상태를 다룰 수 있게 되며, 코드의 가독성과 유지보수성이 향상되는 것이다.
React Query와 SWR은 기존의 전통적인 상태 관리 라이브러리와는 조금 다른 녀석들이다. 서버 상태라는 것을 관리하는 녀석들인데 서버 상태란 애플리케이션이 서버와 통신하여 가져오는 데이터나 정보를 의미한다.
이 서버 상태라는 것이 뭐라고 이것만 따로 관리하는 라이브러리가 생길 정도일까?? 그 이유는 단순하다. 서버상태가 클라이언트 상태보다 조금 더 복잡하고 신경써줘야 하는 요소들이 있기 때문이다.
비동기성
서버 상태는 비동기적으로 변경될 수 있다. 즉, 클라이언트가 서버에 요청을 보낸 후 데이터를 받아오는 동안 다른 작업을 계속할 수 있다. 이 과정은 사용자가 웹 애플리케이션을 사용할 때 자연스럽게 이루어져야 한다.
서버와 클라이언트간의 동기화
서버 상태는 서버의 데이터나 상태가 변경될 때마다 업데이트된다. 예를 들어, 사용자가 데이터를 입력하거나 어떤 작업을 수행할 때 서버의 상태가 바뀐다. 근데 만약 클라이언트가 이를 모른다면 어떻게 될까? 최신의 데이터를 보여줄 수 있을까? 고로 서버 상태는 클라이언트와 서버 간의 동기화를 필요로 한다. 클라이언트가 서버에서 데이터를 가져오고, 이후 변경된 데이터를 다시 서버에 업데이트하는 과정이 필요하다.
캐싱
서버 상태는 종종 캐시될 수 있어야 한다. 매번 똑같은 데이터를 클라이언트에서 요청하면 어떻게 될까? 불필요한 fetch 요청이 이루어지고 서버에 부하가 걸릴 것이다. 이를 보완하기 위해 클라이언트가 서버에 요청을 보내기 전에 이미 가져온 데이터를 캐시에 저장하여, 불필요한 네트워크 요청을 줄이고 성능을 향상시키는 작업이 생겼다.
이처럼 서버 상태는 클라이언트 상태보다는 조금 더 복잡하지만서도 애플리케이션의 동작에 중요한 영향을 미친다는 사실은 동일하다. 고로 이를 효율적으로 관리하는 것이 사용자 경험을 향상시키는 데 매우 중요하다. React Query, SWR과 같은 라이브러리는 서버 상태를 쉽게 관리하고 동기화할 수 있도록 도와주는 녀석들이다.
이번 포스팅에서는 상태란 무엇인지, 어떠한 배경에서 등장했으며, 상태 관리를 한다는 것은 또 무엇인지 마지막으로는 이를 효율적으로 할 수 있도록 도와주는 라이브러리들은 무엇이 있는지에 대한 내용을 다루었다.
스스로 생각하기에 한가지만 기억하고 넘어가야 하는 중요한 사항이 있다면 그것은 바로 상태 관리의 효율성과 직관성은 사용자 경험을 향상시키는 데 매우 필수적이라는 점이다. 이를 위해 적절한 상태 관리 라이브러리를 선택하고 활용하는 것 또한 개발에서 매우 중요한 요소로 작용하는 것 같다.
오늘의 리액트 딥다이브는 여기서 끝!
