Skip to content

필사 모드: 현대 프론트엔드 상태 관리의 진화 — Redux, Zustand, Jotai, Signals, TanStack Query, RSC, XState 완전 해부 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

왜 상태는 프론트엔드의 영원한 난제인가

"HTML은 문서, CSS는 스타일, JS는 동작"이던 시대는 끝났다. **2025년의 UI는 살아있는 상태 기계**다.

- 사용자 입력

- 네트워크 응답

- 웹소켓 푸시

- URL 쿼리 파라미터

- 쿠키·로컬스토리지

- 외부 라이브러리(맵, 에디터, 화상회의)

- 화면 회전·다크모드

- 권한·인증 상태

- 낙관적 업데이트(Optimistic UI) 상태

- 폼의 dirty/touched/validation

- 서버 컴포넌트가 가진 상태

- 에이전트 AI의 스트리밍 토큰

이 모든 것을 **한 프레임워크에 구겨넣던 시대**의 결말이 Redux였다. 그리고 2025년 우리는 "**상태는 한 종류가 아니다**"를 전제한 설계를 택한다.

Part 1 — Redux의 시대 (2015-2020)

왜 Redux가 지배했는가

2015년 Dan Abramov가 Flux의 단점을 개선해 공개. React Conf 데모가 "time-travel debugging"으로 업계를 뒤흔들었다.

**3대 원칙:**

1. Single Source of Truth — 하나의 스토어.

2. State is Read-Only — action을 통해서만 변경.

3. Changes via Pure Functions — reducer는 순수함수.

const reducer = (state = 0, action) => {

switch (action.type) {

case 'INCREMENT': return state + 1;

case 'DECREMENT': return state - 1;

default: return state;

}

};

왜 사람들이 Redux를 떠났는가

2018년경부터 피로감이 쌓였다. 주된 이유:

1. **Boilerplate 폭발** — 기능 하나 추가에 action type, action creator, reducer, selector, thunk 5개 파일.

2. **비동기가 2등 시민** — redux-thunk/saga/observable 중 택하라는 3파전.

3. **서버 상태와 클라이언트 상태 혼재** — 캐싱·재검증·optimistic을 수동으로.

4. **Immutability 강제가 실수의 원천** — `state.items.push()` 한 번에 버그.

5. **useContext + useReducer**가 대부분의 경우 충분해짐.

Redux Toolkit (RTK) — 2020년 구원

공식 팀이 **createSlice + createAsyncThunk + RTK Query**로 응답. 보일러플레이트가 70% 줄었다.

const counterSlice = createSlice({

name: 'counter',

initialState: 0,

reducers: {

increment: state => state + 1,

decrement: state => state - 1,

}

});

Immer를 내장해 "직접 수정하는 것처럼 보이는" 코드가 허용됐다. 그러나 **이미 대안들이 자라고 있었다**.

Part 2 — 미니멀리즘의 3형제 — Zustand, Jotai, Valtio

Zustand — "작고 빠르고 실용적"

Poimandres 팀 제작. 3kb.

const useCounter = create((set) => ({

count: 0,

increment: () => set((s) => ({ count: s.count + 1 })),

decrement: () => set((s) => ({ count: s.count - 1 })),

}));

function Counter() {

const { count, increment } = useCounter();

return <button onClick={increment}>{count}</button>;

}

**특징:**

- Provider 불필요.

- Redux DevTools 연결 가능.

- **선택자로 불필요 렌더 방지** — `useCounter(s => s.count)`.

- 미들웨어(persist, immer, subscribeWithSelector).

**2024년 npm 주간 다운로드: 6M+** — Redux를 거의 따라잡았다.

Jotai — "아톰 단위 상태"

Recoil에서 영감. 각 상태가 "atom".

const countAtom = atom(0);

const doubledAtom = atom((get) => get(countAtom) * 2);

function Counter() {

const [count, setCount] = useAtom(countAtom);

const [doubled] = useAtom(doubledAtom);

return <button onClick={() => setCount(c => c + 1)}>{count} / {doubled}</button>;

}

**장점:**

- 세밀한 구독 — atom 단위로만 리렌더.

- **비동기 atom**도 일급 시민.

- React Suspense와 자연스럽게 통합.

**언제 Jotai?** 많은 독립적 상태 단위가 있고, 컴포넌트 트리에 흩어져 있을 때.

Valtio — "Proxy 기반, mutable처럼"

const state = proxy({ count: 0 });

function Counter() {

const snap = useSnapshot(state);

return <button onClick={() => state.count++}>{snap.count}</button>;

}

**Proxy 기반**으로 mutable하게 작성. 접근한 속성만 구독. 객체 중심 앱(게임·에디터)에서 편하다.

Part 3 — Signals의 부활

Signal의 아이디어 (KnockoutJS 2010, Vue 2014, SolidJS 2020)

"값 + 구독자 목록을 갖는 반응형 프리미티브."

// SolidJS

const [count, setCount] = createSignal(0);

return <button onClick={() => setCount(count() + 1)}>{count()}</button>;

// 코드만 보면 React 같지만, 리렌더가 발생하지 않는다.

// 변경된 텍스트 노드만 갱신된다.

왜 다시 주목받는가

React의 "컴포넌트 단위 리렌더"는 대규모 앱에서 성능 천장. Signals는 **더 세밀한 반응성**으로 해결.

**2023-2024 Signals 채택 흐름:**

- **SolidJS** — signals가 프레임워크의 핵.

- **Preact** — `@preact/signals`.

- **Vue 3 Composition API** — `ref`/`computed`가 실질적 signal.

- **Svelte 5 Runes** (2024) — `$state`, `$derived`, `$effect`로 signal화.

- **Angular 16+ Signals** (2023) — 공식 도입.

- **React** — 2024 TC39 Signals proposal 참여. 단, 네이티브 도입은 보수적.

React는 왜 주저하는가

React 팀의 입장: "Signal은 강력하지만 **컴파일러(React Compiler)가 더 낫다**." 2024년 RC 공개된 **React Compiler**가 Memoization을 자동화해 Signal의 성능 이점을 상당 부분 흡수.

**그럼에도** `@preact/signals-react` 같은 라이브러리로 React에서도 쓸 수 있다. 세밀한 제어가 필요한 곳(차트·에디터)에 자주 등장.

Part 4 — 서버 상태의 분리 — TanStack Query 혁명

깨달음 (2019, Tanner Linsley)

"**서버 상태와 클라이언트 상태는 근본적으로 다르다.**"

| | 클라이언트 상태 | 서버 상태 |

|---|---|---|

| 소유자 | 앱 | 서버 |

| 최신성 | 항상 최신 | 오래될 수 있음 |

| 동기화 | 불필요 | 필수(polling/invalidation) |

| 캐싱 | 선택 | 거의 필수 |

| 중복 요청 제거 | 불필요 | 필수 |

이 깨달음으로 React Query(지금의 TanStack Query)가 태어났다.

핵심 기능

const { data, isLoading, error } = useQuery({

queryKey: ['users', userId],

queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),

staleTime: 60_000,

});

- **자동 캐싱** — queryKey 기반.

- **중복 제거(Dedup)** — 동시에 같은 쿼리 호출 시 1회만.

- **백그라운드 재검증** — 창 재집중, 네트워크 복구 시.

- **Optimistic Updates** — `onMutate`로 롤백 가능한 낙관적 변경.

- **Infinite Queries** — 페이지네이션.

- **Prefetching** — 마우스 호버 시 미리 가져오기.

Mutation

const mutation = useMutation({

mutationFn: updateUser,

onMutate: async (newUser) => {

await queryClient.cancelQueries({ queryKey: ['users', newUser.id] });

const prev = queryClient.getQueryData(['users', newUser.id]);

queryClient.setQueryData(['users', newUser.id], newUser);

return { prev };

},

onError: (err, newUser, context) => {

queryClient.setQueryData(['users', newUser.id], context.prev);

},

onSettled: (_, __, { id }) => {

queryClient.invalidateQueries({ queryKey: ['users', id] });

},

});

**4단계(onMutate → onError → onSuccess → onSettled)**로 낙관적 UI를 안전하게.

경쟁자들

- **SWR** (Vercel, 2019) — TanStack Query보다 간단, 기능 적음.

- **RTK Query** — Redux 쓰는 팀에 편리.

- **Apollo Client** (GraphQL) — GraphQL 생태계 표준.

- **urql** (GraphQL) — 더 가벼운 대안.

**2025년 현실:** 새 프로젝트는 대부분 TanStack Query. GraphQL이면 Apollo 또는 urql.

Part 5 — RSC(React Server Components)가 바꾼 것

RSC의 모델 (2020 제안, 2023 Next.js App Router로 대중화)

**일부 컴포넌트는 서버에서만 실행.** 데이터 fetching이 컴포넌트와 함께 선언된다.

// 서버 컴포넌트 (기본)

async function UserList() {

const users = await db.user.findMany(); // 서버에서 직접 DB 접근

return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;

}

// 클라이언트 컴포넌트 (명시)

'use client';

function LikeButton() {

const [liked, setLiked] = useState(false);

return <button onClick={() => setLiked(!liked)}>{liked ? '❤' : '♡'}</button>;

}

RSC가 바꾸는 상태의 개념

- **"서버 상태 fetching"이 더 이상 클라이언트의 문제가 아닌 경우가 많다.**

- 초기 렌더에 필요한 데이터는 서버 컴포넌트에서 가져온다.

- 클라이언트 상태는 **진짜 상호작용 부분**에만 집중.

- TanStack Query의 상당 부분이 **서버 측 데이터 로딩**으로 대체.

Server Actions

// app/actions.ts

'use server';

export async function updateUser(formData) {

await db.user.update({ where: {...}, data: {...} });

revalidatePath('/users');

}

// 컴포넌트에서 그대로 호출

**RPC 같은 모델**이 React에 들어왔다. REST/GraphQL을 생략할 수 있는 경우가 늘었다.

현실의 혼란

2024-2025년 프론트엔드 커뮤니티의 주된 논쟁:

- "RSC는 복잡도를 제거하는가, 추가하는가?"

- "클라이언트 컴포넌트 + TanStack Query vs 서버 컴포넌트, 어떤 게 더 나은가?"

- **Remix(=React Router 7)**는 RSC 대신 loader/action 모델을 다듬었다.

- **TanStack Start**(2024) — RSC를 대체하는 새로운 메타프레임워크.

**답은 "둘 다 쓴다"**로 수렴 중. 서버 컴포넌트는 초기 로딩, 클라이언트 상태는 상호작용.

Part 6 — 폼 상태 — 왜 따로 관리하는가

폼은 특별하다

- 값(value)

- 에러(error)

- touched / dirty

- 제출 중(submitting)

- 제출 성공/실패

- 검증 규칙

- 필드 간 의존성

이걸 글로벌 상태에 넣으면 **폼 하나가 앱의 생명주기를 복잡하게** 만든다. 그래서 폼 전용 라이브러리가 필요하다.

React Hook Form

2019년 등장. **언컨트롤드 컴포넌트 + ref 기반**으로 리렌더 최소화.

const { register, handleSubmit, formState: { errors } } = useForm();

return (

{errors.email && <span>Error</span>}

);

**2024년 npm 주간 다운로드 7M+.** 업계 표준.

TanStack Form (2024)

Tanner Linsley가 직접. 프레임워크 중립(React/Vue/Solid/Lit). 타입 안전성이 업계 최상위.

Formik — 쇠퇴

2018-2020년 지배했지만 성능·타입 이슈로 React Hook Form에 추월. **유지보수 모드.**

Zod + 폼

2024-2025년 사실상 표준:

const schema = z.object({

email: z.string().email(),

age: z.number().min(18),

});

// React Hook Form + zodResolver

useForm({ resolver: zodResolver(schema) });

Part 7 — XState — 복잡한 로직을 상태 머신으로

왜 상태 머신인가

UI 로직의 대부분은 **"상태 전이"의 집합**이다. 그런데 if/else로 흩어지면 "불가능한 상태"가 생긴다.

loading && error → 불가능해야 함

loading && data → 불가능해야 함

**XState**는 이걸 수학적으로 정리한다.

const fetchMachine = createMachine({

initial: 'idle',

states: {

idle: { on: { FETCH: 'loading' } },

loading: {

on: {

RESOLVE: 'success',

REJECT: 'failure',

}

},

success: { type: 'final' },

failure: { on: { RETRY: 'loading' } }

}

});

**언제 XState가 빛나는가:**

- 결제 플로우

- 화상회의·미디어 스트림

- 문서 편집기의 dirty/saved 상태

- 다단계 위저드

- 온보딩 흐름

**단점:** 학습 곡선. 작은 앱엔 과도.

Stately Studio

시각적 상태 머신 편집기. "디자이너가 상태를 설계, 개발자가 받아서 구현"하는 워크플로 가능.

Part 8 — 상태 정규화 & 관계

중첩 구조의 함정

// 나쁜 예

state.users[0].posts[3].comments[2].author.name = 'New';

같은 author가 여러 곳에서 중첩된다. 어디 하나 갱신하면 다른 곳은 낡는다.

정규화

{

users: { 1: { id: 1, name: 'A', postIds: [10, 11] } },

posts: { 10: { id: 10, title: '...', authorId: 1, commentIds: [100] } },

comments: { 100: { id: 100, text: '...', authorId: 1 } },

}

**Normalizr** 라이브러리가 이 작업을 도와준다. Redux 시대에 유행했지만, TanStack Query가 `select`로 비슷한 효과를 주면서 최근엔 덜 쓰인다.

Part 9 — URL도 상태다

URL은 무료 전역 상태

- 공유 가능

- 뒤로가기/앞으로가기 자동

- SEO

- 딥링크

nuqs (2024)

const [filter, setFilter] = useQueryState('filter');

URL 쿼리 파라미터를 상태처럼. Next.js App Router와 완벽 통합. **필터·정렬·페이지네이션·모달 열림**은 URL 상태여야 한다.

Part 10 — Undo/Redo와 시간 여행

Command Pattern 기반

class UndoableStore {

history = [];

index = -1;

execute(command) {

this.history = this.history.slice(0, this.index + 1);

this.history.push(command);

this.index++;

command.do();

}

undo() {

if (this.index >= 0) {

this.history[this.index].undo();

this.index--;

}

}

}

Immer의 patches

const [nextState, patches, inversePatches] = produceWithPatches(state, draft => {

draft.todos.push({ text: 'New' });

});

patches/inversePatches로 정확한 undo/redo.

Yjs — 협업 편집의 상태

CRDT(Conflict-free Replicated Data Type) 구현. **실시간 협업 앱**의 사실상 표준.

- Notion, Linear, Figma 같은 앱의 기반.

- 여러 사용자의 편집을 병합하면서 충돌 없이 수렴.

Part 11 — 실무 결정 가이드

상태의 5분류

1. **서버 상태** → TanStack Query / SWR / RTK Query

2. **URL 상태** → nuqs / next/navigation / 직접

3. **글로벌 클라이언트 상태** → Zustand / Jotai / Redux Toolkit

4. **로컬 UI 상태** → useState / useReducer

5. **폼 상태** → React Hook Form / TanStack Form

6. **복잡한 플로우** → XState

**황금률:** 서로 다른 종류의 상태를 **같은 도구로 관리하지 말라**.

2025년 새 프로젝트 기본 스택 (개인 추천)

- Framework: **Next.js (App Router)** or **TanStack Start** or **Remix/RR7**

- Server State: **TanStack Query** (클라이언트 컴포넌트에서)

- Client State: **Zustand** (간단) or **Jotai** (atom 단위)

- Form: **React Hook Form + Zod**

- URL: **nuqs**

- 특수 로직: **XState** (필요 시)

- Realtime/Collab: **Yjs**

Part 12 — 실무 체크리스트 (12항목)

1. **서버 상태와 클라이언트 상태 분리** — 이게 대전제.

2. **글로벌 상태는 최소화** — 대부분은 로컬이면 충분.

3. **URL에 담을 수 있는 건 URL로** — 공유 가능성을 얻는다.

4. **폼은 전용 라이브러리** — 글로벌 상태에 넣지 말라.

5. **Optimistic UI는 롤백과 함께** — 실패 케이스 UX가 핵심.

6. **무한 리렌더링 근본 원인은 90% 객체 참조** — useMemo/selectors.

7. **Suspense를 활용하라** — 로딩 상태를 선언적으로.

8. **React DevTools Profiler**로 정기 측정.

9. **XState는 '정말 복잡한' 곳에만** — CRUD에는 과잉.

10. **폼 검증은 Zod + 스키마** — 프론트·백엔드 공유.

11. **서버 컴포넌트를 먼저 시도**하라 — 필요할 때만 클라이언트.

12. **상태 트리를 그림으로 그려라** — 문서화의 가장 큰 효과.

Part 13 — 10대 안티패턴

1. **모든 걸 Redux에** — 로컬 UI 상태까지 글로벌화.

2. **서버 데이터를 Redux에 넣고 수동 캐싱** — TanStack Query가 할 일.

3. **폼을 전역 상태에** — 페이지 전환 시 혼돈.

4. **URL 상태를 글로벌에 복제** — 진실의 두 소스.

5. **useState 연쇄로 복잡 로직 모델링** — XState가 맞는 순간이 있다.

6. **setState 후 바로 읽기** — 비동기성 무시.

7. **useEffect로 데이터 fetching** — 2024년 이후는 TanStack Query / RSC.

8. **컴포넌트 트리 전체에 Context** — 리렌더 폭주.

9. **깊은 중첩 없이 정규화만** — 작은 앱에 Normalizr 오버엔지.

10. **"이거 한 줄로 해결돼요"** — 1년 후 기술부채.

마치며 — 상태는 "모델링"이다

React 15(2016)부터 React 19(2024)까지 9년간 프론트엔드가 배운 한 가지는:

> **상태 관리는 라이브러리 선택의 문제가 아니라, 상태 자체를 올바르게 모델링하는 문제다.**

Redux가 실패한 게 아니다. "모든 상태는 하나의 글로벌 스토어"라는 전제가 틀렸을 뿐이다. 2025년 우리는 **상태에 종류가 있음**을 인정하고, 각각에 맞는 도구를 쓴다.

좋은 상태 설계는:

- **불가능한 조합을 만들 수 없도록**

- **최소한의 진실(single source of truth)로**

- **파생(derived)을 명시적으로**

- **사이드 이펙트를 예측 가능하게**

이 네 가지를 달성하는 것이다. 어떤 라이브러리든 이 원칙을 지키면 옳다.

다음 글 예고 — "웹 보안 방어의 실전" — XSS, CSRF, SSRF, Clickjacking, Prototype Pollution, Supply Chain, CORS 실무 가이드

보안 기본(이전 Security 글)은 개념이었다. 다음은 **웹 프론트엔드·백엔드 개발자가 매일 마주치는 공격과 방어**다.

- **XSS 세분화** — Reflected / Stored / DOM-based 각각의 방어

- **CSP를 처음부터 구축하는 법** — nonce, hash, strict-dynamic

- **Trusted Types**로 XSS를 원천 차단

- **CSRF와 SameSite** — 2025년 기본값의 의미

- **SSRF** — Capital One 사건과 AWS IMDSv2

- **Clickjacking**과 X-Frame-Options / frame-ancestors

- **Prototype Pollution** — Node 생태계의 고질병

- **Supply Chain Attack** — event-stream, ua-parser-js, xz-utils

- **CORS 완전 이해** — preflight, credentials, wildcard의 함정

- **Rate Limiting과 Bot Defense** — Cloudflare Turnstile, hCaptcha

"앱을 해킹당하지 않는 법"의 실무편. 다음 글에서.

현재 단락 (1/307)

"HTML은 문서, CSS는 스타일, JS는 동작"이던 시대는 끝났다. **2025년의 UI는 살아있는 상태 기계**다.

작성 글자: 0원문 글자: 10,087작성 단락: 0/307