Skip to content

Commit cb3a0e1

Browse files
committed
docs: Redux 공식문서 번역, Normalizing State Shape
1 parent 818e9ce commit cb3a0e1

File tree

1 file changed

+386
-0
lines changed

1 file changed

+386
-0
lines changed
Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
---
2+
title: "[Redux 공식문서 번역] Normalizing State Shape"
3+
createdAt: 2025-10-31
4+
category: React
5+
description: "Redux 애플리케이션에서 상태 구조를 정규화하는 방법에 대해 알아봅니다. 이 글은 Redux 공식 번역이 아니며, 개인 학습 및 공유 목적으로 작성된 비공식 번역입니다. (원문 : https://redux.js.org/usage/structuring-reducers/normalizing-state-shape)"
6+
comment: true
7+
head:
8+
- - meta
9+
- name: keywords
10+
content: Redux, Normalizing State Shape, Redux 공식문서 번역, 전역상태관리, React, ReduxJS, Redux 공식문서 번역, ReduxToolkit 공식문서 번역
11+
---
12+
13+
:::warning ⚠️ 이 글은 Redux 공식 번역이 아니며, 개인 학습 및 공유 목적으로 작성된 비공식 번역입니다.
14+
15+
본 글은 Redux 공식문서의
16+
[Normalizing State Shape 섹션](https://redux.js.org/usage/structuring-reducers/normalizing-state-shape)을 비공식 번역한 내용입니다. <br/>
17+
원문 저작권은 Dan Abramov 및 Redux 기여자들에게 있으며, MIT License 에 따라 사용되었습니다.
18+
:::
19+
20+
얼마전, Redux 를 사용하면서 상태를 업데이트하는 reducer 로직을 작성했는데, <br/>
21+
상태가 점점 깊게 중첩되고, 해당 상태를 업데이트하기위해 순회를 반복하는 코드가 많아지는 것을 발견했습니다. <br/>
22+
23+
```ts
24+
// 해당 아이템과 카테고리에 속한 아이템을 찾아서 업데이트하는 예시
25+
// 시간복잡도 = O(n)
26+
export function updateItem(
27+
state: State,
28+
action: PayloadAction<{ itemId: string; newValue: string }>,
29+
) {
30+
const { itemId, newValue } = action.payload;
31+
return produce(state, (draft) => {
32+
const category = draft.categories.find((cat) =>
33+
cat.items.some((item) => item.id === itemId),
34+
);
35+
if (category) {
36+
const item = category.items.find((item) => item.id === itemId);
37+
if (item) {
38+
item.value = newValue;
39+
}
40+
}
41+
});
42+
}
43+
```
44+
45+
아이템의 개수가 작을때는 문제가 없겠지만, 아이템이 많아지면 매번 순회하면서 찾아야 하므로 성능에 문제가 생길 수 있겠다는 생각이 들었습니다. <br/>
46+
47+
> 상태를 업데이트 할 때 마다 $O(n)$ 의 시간복잡도가 발생하기 때문이죵..
48+
49+
백엔드 친구랑 이야기하다가, 데이터베이스에서 데이터를 정규화(Normalization) 하는 개념이 떠올랐고, <br/>
50+
Redux 상태도 정규화된 형태로 관리하면 좋겠다는 생각이 들어 공식문서를 찾아보게 되었습니다. <br/>
51+
52+
실제로 Redux 공식문서에서도 상태를 정규화하는 방법을 권장하고 있었고, <br/>
53+
이번 포스트에서는 해당 내용을 정리해보았습니다. <br/>
54+
55+
## Normalizing State Shape
56+
57+
많은 애플리케이션은 중첩되거나 관계형 구조의 데이터를 다룹니다. <br/>
58+
예를 들어, 블로그 편집기에는 여러 게시글(Post) 이 있을 수 있고, 각 게시글에는 여러 댓글(Comment) 이 달릴 수 있으며, 게시글과 댓글 모두 사용자(User) 가 작성합니다. <br/>
59+
이런 종류의 애플리케이션 데이터는 다음과 같은 형태를 가질 수 있습니다.
60+
61+
```js
62+
const blogPosts = [
63+
{
64+
id: "post1",
65+
author: { username: "user1", name: "User 1" },
66+
body: "......",
67+
comments: [
68+
{
69+
id: "comment1",
70+
author: { username: "user2", name: "User 2" },
71+
comment: ".....",
72+
},
73+
{
74+
id: "comment2",
75+
author: { username: "user3", name: "User 3" },
76+
comment: ".....",
77+
},
78+
],
79+
},
80+
{
81+
id: "post2",
82+
author: { username: "user2", name: "User 2" },
83+
body: "......",
84+
comments: [
85+
{
86+
id: "comment3",
87+
author: { username: "user3", name: "User 3" },
88+
comment: ".....",
89+
},
90+
{
91+
id: "comment4",
92+
author: { username: "user1", name: "User 1" },
93+
comment: ".....",
94+
},
95+
{
96+
id: "comment5",
97+
author: { username: "user3", name: "User 3" },
98+
comment: ".....",
99+
},
100+
],
101+
},
102+
// and repeat many times
103+
];
104+
```
105+
106+
데이터 구조가 다소 복잡하고, 일부 데이터가 중복되어 있다는 점에 주목하세요.
107+
이런 문제는 다음과 같은 문제가 있습니다
108+
109+
#### 1. 데이터 중복 문제
110+
111+
- 동일한 데이터가 여러 곳에 복제되어 있으면, 해당 데이터가 변경될 때 모든 복제본을 올바르게 업데이트하기가 어렵습니다.
112+
113+
#### 2. 중첩 구조로 인한 복잡성 증가
114+
115+
- 데이터가 중첩되어 있으면, 그에 따른 리듀서(reducer) 로직도 더 깊고 복잡해집니다.
116+
- 특히 깊이 중첩된 필드를 업데이트하려 할 때 코드가 매우 지저분해지고 유지보수가 어려워질 수 있습니다.
117+
118+
#### 3. 불변성 (Immutability) 으로 인한 성능 문제
119+
120+
- Redux 상태는 불변성을 유지해야 하므로, 중첩된 데이터를 업데이트할 때는 해당 데이터뿐만 아니라 그 상위(ancestor) 객체들도 모두 복사 및 갱신해야 합니다.
121+
- 이렇게 새로운 객체 참조가 생성되면, 실제로 데이터가 바뀌지 않은 UI 컴포넌트까지 불필요하게 리렌더링될 수 있습니다.
122+
123+
이러한 이유로 Redux에서는 관계형(relational) 또는 중첩된(nested) 데이터를 다룰 때,
124+
스토어의 일부를 데이터베이스처럼 취급하고, 데이터를 정규화(normalized) 형태로 관리하는 것이 권장됩니다.
125+
126+
## Designing a Normalized State (정규화된 상태 설계)
127+
128+
데이터를 정규화(Normalizing) 하는 기본 개념은 다음과 같습니다
129+
130+
#### 1. 각 데이터 유형별로 "테이블" 을 분리합니다.
131+
132+
예를들어, `users`, `posts`, `comments` 처럼 각 엔티티마다 독립된 테이블을 만듭니다.
133+
134+
#### 2. 각 "데이터 테이블" 은 객체 형태로 아이템을 저장합니다.
135+
136+
키는 해당 아이템의 ID, 값은 실제 아이템 객체로 구성합니다
137+
138+
```js
139+
{
140+
posts: {
141+
1: { id: 1, title: "첫 게시글" },
142+
2: { id: 2, title: "Redux 소개" },
143+
}
144+
}
145+
```
146+
147+
#### 3. 다른 데이터를 참조할 때는 ID만 저장합니다.
148+
149+
예를들어, 댓글(comment) 에서 작성자(user) 를 직접 포함하지 않고, 작성자의 `userId` 만 저장합니다.
150+
151+
```js
152+
{
153+
comments: {
154+
1: { id: 1, userId: 2, comment: "멋진 글이네요!" },
155+
2: { id: 2, userId: 3, comment: "많은 도움이 되었습니다." },
156+
}
157+
}
158+
```
159+
160+
#### 4. 배열은 ID들의 순서를 표현하는 용도로 사용합니다.
161+
162+
예를들어, 특정 게시물의 댓글 순서를 `[5, 7, 9]` 같은 ID 배열로 표현합니다.
163+
164+
```js
165+
{
166+
postComments: {
167+
1: [5, 7, 9], // 게시물 ID 1의 댓글 ID 배열
168+
2: [10, 11], // 게시물 ID 2의 댓글 ID 배열
169+
}
170+
}
171+
```
172+
173+
#### 블로그 예제의 정규화
174+
175+
블로그 예제에 대한 정규화된 상태 구조는 다음과 같이 나타날 것입니다.
176+
177+
```js
178+
{
179+
posts: {
180+
byId: {
181+
post1: {
182+
id: "post1",
183+
author: "user1",
184+
body: "......",
185+
comments: ["comment1", "comment2"]
186+
},
187+
post2: {
188+
id: "post2",
189+
author: "user2",
190+
body: "......",
191+
comments: ["comment3", "comment4", "comment5"]
192+
}
193+
},
194+
allIds: ["post1", "post2"]
195+
},
196+
comments: {
197+
byId: {
198+
comment1: {
199+
id: "comment1",
200+
author: "user2",
201+
comment: "....."
202+
},
203+
comment2: {
204+
id: "comment2",
205+
author: "user3",
206+
comment: "....."
207+
},
208+
comment3: {
209+
id: "comment3",
210+
author: "user3",
211+
comment: "....."
212+
},
213+
comment4: {
214+
id: "comment4",
215+
author: "user1",
216+
comment: "....."
217+
},
218+
comment5: {
219+
id: "comment5",
220+
author: "user3",
221+
comment: "....."
222+
}
223+
},
224+
allIds: ["comment1", "comment2", "comment3", "comment4", "comment5"]
225+
},
226+
users: {
227+
byId: {
228+
user1: {
229+
username: "user1",
230+
name: "User 1"
231+
},
232+
user2: {
233+
username: "user2",
234+
name: "User 2"
235+
},
236+
user3: {
237+
username: "user3",
238+
name: "User 3"
239+
}
240+
},
241+
allIds: ["user1", "user2", "user3"]
242+
}
243+
}
244+
```
245+
246+
이 상태 구조는 전체적으로 훨씬 Flat(평평)합니다. <br/>
247+
기존의 중첩된 구조에 비해 여러 면에서 개선된 형태입니다.
248+
249+
#### 1. 데이터 일관성 유지
250+
251+
각 항목이 오직 한 곳에서만 정의되어 있기 때문에, <br/>
252+
어떤 데이터가 변경될 때 여러 위치를 동시에 수정할 필요가 없습니다.
253+
254+
#### 2. Reducer 로직 단순화
255+
256+
데이터가 깊이 중첩되어 있지 않으므로, <br/>
257+
Reducer 가 깊은 단계까지 접근하거나 복사할 필요가 없어집니다. <br/>
258+
즉, 상태 업데이트 로직이 훨씬 단순하고 명확해집니다.
259+
260+
#### 3. 일관된 데이터 접근 방식
261+
262+
특정 아이템을 가져오거나 수정할 때, 그 아이템의 타입(type)과 ID만 알면 됩니다. <br/>
263+
다른 객체 속을 파고들 필요 없이, `state[entityType].entities[id]` 처럼 일관된 접근이 가능합니다.
264+
265+
#### 4. 불필요한 리렌더링 최소화
266+
267+
데이터 타입이 분리되어 있기 때문에, <br/>
268+
예를들어 댓글(Comment)의 내용을 바꾸더라도, `comments > byId > comment` 부분만 새로 복사하면 됩니다.
269+
270+
즉, 전체 트리의 일부분만 변경되므로, 실제로 변경된 데이터와 관련된 컴포넌트만 리렌더링됩니다.
271+
272+
<br/>
273+
274+
반면, 원래의 중첩 구조에서는 댓글 하나를 수정하더라도 <br/>
275+
276+
- 댓글 객체 (Comment)
277+
- 부모 게시글 (Post)
278+
- 게시글 배열 전체 (Posts[])
279+
280+
가 모두 업데이트되어야 하고, 결과적으로 모든 `Post`, `Comment` 컴포넌트가 불필요하게 다시 렌더링되었을 것입니다.
281+
282+
#### 5. 컴포넌트 연결 구조의 변화와 성능 향상
283+
284+
정규화된 상태 구조를 사용하면, 일반적으로 더 많은 컴포넌트가 직접 store 에 연결되고, <br/>
285+
각 컴포넌트가 자신의 데이터만 조회하게 됩니다.
286+
287+
즉, 상위 컴포넌트가 대량의 데이터를 조회하여 자식에게 전부 내려주는 방식 대신, <br/>
288+
상위 컴포넌트는 `ID` 만 전달하고, 자식 컴포넌트는 `자신의 ID 기반으로 데이터를 직접 구독`합니다.
289+
290+
이 패턴은 React Redux 애플리케이션에서 UI 성능 최적화에 매우 효과적이며, <br/>
291+
결국 상태를 정규화 하는것이 성능 개선의 핵심적인 역할을 하게 됩니다.
292+
293+
## Organizing Normalized Data in State (정규화된 데이터를 상태로 구성하기)
294+
295+
일반적인 애플리케이션은 관계형 데이터와 비관계형 데이터가 섞여 있습니다. <br/>
296+
이 서로 다른 데이터 타입을 정확이 어떻게 구성해야 한다는 규칙은 없지만, 흔히 쓰는 패턴 중 하나는 관계형 "테이블"들을 `entities` 같은 공통 상위 키 아래에 모으는 방식입니다.
297+
298+
이 접근을 활용한 상태 구조 예시는 다음과 같습니다.
299+
300+
```js
301+
{
302+
simpleDomainData1: { .... },
303+
simpleDomainData2: { .... },
304+
entities: {
305+
entityType1: { .... },
306+
entityType2: { .... }
307+
},
308+
ui: {
309+
uiSection1: { .... },
310+
uiSection2: { .... }
311+
}
312+
}
313+
```
314+
315+
이 구조는 여러 방식으로 확장할 수 있습니다. <br/>
316+
예를들어 엔티티 편집 기능이 수많은 애플리케이션이라면, 상태에 "테이블" 을 두번 두는 전략이 유용할 수 있습니다. <br/>
317+
318+
하나는 `current` (현재 값), 다른 하나는 `work-in-progress` (편집 중인 값) 용도로 사용하는 것입니다.
319+
320+
항목을 편집할 때는 해당 값을 `work-in-progress` 영역으로 복사하고, 이후의 업데이트 액션은 `work-in-progress` 복사본에만 적용합니다. <br/>
321+
이렇게 하면 편집 폼은 편집본을 기준으로 제어되는 동안, UI 의 다른 부분은 원본을 계속 참조 할 수 있습니다.
322+
323+
- Reset(초기화) : `work-in-progress` 에서 해당 항목을 제거하고, `current` 의 원본 데이터를 다시 `work-in-progress`로 복사합니다.
324+
- Apply(적용) : `work-in-progress`의 값을 `current` 영역으로 복사하여 편집 내용을 실제 값으로 반영합니다.
325+
326+
## Relationship and Tables (관계와 테이블 구조)
327+
328+
Redux 스토어의 일부를 "데이터베이스" 처럼 다루기로 했기 때문에, 데이터베이스 설계의 원칙들이 이곳에도 동일하게 적용됩니다.
329+
330+
예를들어 Many-To-Many 관계가 존재하는 경우, 두 엔티티 간의 연결을 표현하기 위해 별도의 `조인 테이블(join table)` 을 만들어야 할 수도 있습니다. <br/>
331+
332+
### 예시 : `authors``books` 의 Many-To-Many 관계
333+
334+
```js
335+
{
336+
entities: {
337+
authors: {
338+
byId: {},
339+
allIds: []
340+
},
341+
books: {
342+
byId: {},
343+
allIds: []
344+
},
345+
authorBook: {
346+
byId: {
347+
1: { id: 1, authorId: 5, bookId: 22 },
348+
2: { id: 2, authorId: 5, bookId: 15 },
349+
3: { id: 3, authorId: 42, bookId: 12 }
350+
},
351+
allIds: [1, 2, 3]
352+
}
353+
}
354+
}
355+
```
356+
357+
#### 조회 쿼리 예시 : 특정 작가의 모든 책 조회
358+
359+
이제 "이 작가가 쓴 모든 책을 조회" 하는 쿼리를 작성할때, `authorBook` 테이블을 한번 순회하는 것만으로 가능합니다.
360+
361+
```js
362+
const getBooksByAuthor = (state, authorId) => {
363+
return state.entities.authorBook.allIds
364+
.map((id) => state.entities.authorBook.byId[id])
365+
.filter((rel) => rel.authorId === authorId)
366+
.map((rel) => state.entities.books.byId[rel.bookId]);
367+
};
368+
```
369+
370+
일반적인 클라이언트 애플리케이션의 데이터 양과 현대 JavaScript 엔진의 처리 속도를 고려하면, <br/>
371+
이러한 방식의 관계 탐색은 대부분의 상황에서 충분히 빠르게 동작합니다.
372+
373+
즉, Redux 스토어를 데이터베이스처럼 설계하는 것은 <br/>
374+
`데이터 일관성 유지`, `쿼리 단순화`, `성능 최적화` 측면에서 모두 합리적인 접근입니다.
375+
376+
## Normalizing Nested Data (중첩 데이터 정규화)
377+
378+
대부분의 API 는 데이터를 중첩된 형태(Nested Form) 으로 반환합니다. <br/>
379+
이 데이터를 그대로 Redux 상태 트리에 넣으면 관리와 업데이트가 어렵기 때문에, <br/>
380+
Store 에 포함시키기 전에 정규화(Normalization) 작업을 수행하는 것이 좋습니다.
381+
382+
~~이 작업에는 일반적으로 `Normalizr` 라이브러리가 사용됩니다~~
383+
384+
::: info 💬 NOTE
385+
하지만.. deprecated 되었으므로, ReduxToolkit 에서 제공하는 `createEntityAdapter` 를 활용하는게 좋아 보입니다.
386+
:::

0 commit comments

Comments
 (0)