27. 정규화(Normalization) - 복잡한 데이터 관리
25편의 블로그 앱에서 게시글과 댓글, 작성자 정보가 서로 얽히기 시작하면 배열 그대로 상태에 저장하는 방식이 한계를 드러냅니다. 이 편은 **정규화(Normalization)**라는, 관계형 데이터베이스에서 가져온 개념으로 이 문제를 해결합니다.
학습 목표
- 중첩·중복된 배열 구조가 업데이트 시 어떤 문제를 일으키는지 구체적인 버그로 설명할 수 있다.
- ID를 키로 쓰는 정규화된 상태 구조를 직접 설계할 수 있다.
createEntityAdapter로 정규화된 CRUD 로직을 자동 생성할 수 있다.
문제: 중첩된 배열 구조의 중복 업데이트
게시글 목록에 작성자 정보가 통째로 포함된 구조를 생각해봅시다.
| |
김리덕스가 닉네임을 바꾸면 어떻게 될까요? 이 사람이 쓴 모든 게시글의 author 필드를 찾아 전부 업데이트해야 합니다. 게시글이 수백 개라면 리듀서 안에서 state.posts.map(...)으로 전체를 순회하며 조건에 맞는 항목을 찾아 고쳐야 하고, 하나라도 놓치면 화면에 낡은 이름이 남는 버그가 생깁니다.
| |
해결책: ID를 키로 쓰는 정규화된 구조
관계형 데이터베이스가 데이터를 중복 없이 테이블로 나누듯, Redux 상태도 엔티티 종류별로, ID를 키로 하는 객체로 나눠 저장할 수 있습니다.
| |
이제 김리덕스의 이름을 바꾸는 일은 users.byId[7] 한 곳만 수정하면 끝납니다.
| |
게시글이 몇 개든 관계없이 한 번의 객체 속성 갱신으로 끝납니다. 화면에서 게시글의 작성자 이름을 표시할 때는 state.users.byId[post.authorId].name처럼 참조로 조인합니다.
정규화된 데이터를 위한 selector
14편의 selector 패턴이 여기서 특히 중요해집니다. 컴포넌트가 정규화된 구조를 직접 알 필요 없이, selector가 “조인"을 대신 수행합니다.
| |
createEntityAdapter: 정규화 로직 자동 생성
이 정규화 패턴(byId/allIds 구조, CRUD 리듀서, 관련 selector)은 매우 반복적이기 때문에, RTK는 createEntityAdapter로 이를 표준화합니다.
| |
createEntityAdapter가 만드는 상태 구조는 앞서 손으로 만든 byId/allIds와 사실상 동일하며, 필드 이름만 entities/ids로 관례화되어 있습니다. addOne, updateOne, removeOne, setAll 같은 리듀서 함수를 직접 구현할 필요 없이 그대로 reducers에 연결할 수 있고, getSelectors()가 selectAll, selectById 같은 표준 selector도 함께 만들어줍니다.
selectId와 sortComparer: 기본 동작을 커스터마이징하기
createEntityAdapter는 기본적으로 각 엔티티의 id 필드를 키로 쓰고, 삽입 순서대로 ids 배열을 유지합니다. 하지만 실무 데이터가 항상 이 가정에 맞는 것은 아닙니다.
| |
selectId를 지정하지 않으면 createEntityAdapter는 각 엔티티에 .id 속성이 있다고 가정합니다. 백엔드가 _id나 postId 같은 다른 이름을 쓴다면, selectId로 “이 엔티티에서 고유 키를 어떻게 뽑아낼지"를 알려줘야 합니다.
| |
sortComparer를 지정하면 ids 배열이 삽입 순서가 아니라 항상 이 비교 함수 기준으로 정렬된 상태를 유지합니다. selectAllPosts로 뽑은 목록이 이미 정렬돼 있으므로, 컴포넌트에서 매번 .sort()를 호출할 필요가 없습니다.
서버 응답을 정규화하기: setAll
25편의 getPosts 쿼리가 반환하는 배열을 postsReceived로 dispatch하면, setAll이 배열을 자동으로 정규화된 구조로 변환합니다.
| |
24편의 RTK Query와 결합할 때는, getPosts 쿼리의 결과를 컴포넌트에서 그대로 배열로 쓰거나, 필요하다면 transformResponse 옵션으로 응답 자체를 정규화된 형태로 변환할 수도 있습니다.
정규화가 항상 필요한 것은 아니다
정규화는 다음 상황에서 뚜렷한 이점을 줍니다.
- 같은 엔티티(작성자, 태그 등)가 여러 목록에 중복해서 나타난다.
- 엔티티 하나를 업데이트/삭제하는 연산이 빈번하다.
- ID로 특정 항목 하나를 빠르게 찾아야 하는 경우가 많다(
byId[id]는 O(1), 배열find()는 O(n)).
반대로 게시글 하나에 딸린 댓글처럼, 다른 곳에서 재사용되지 않고 항상 부모와 함께 다뤄지는 데이터는 굳이 정규화하지 않아도 됩니다. 모든 상태를 정규화하는 것은 오히려 불필요한 복잡도를 더할 수 있으므로, “중복이 실제로 문제가 되는 데이터"에 선택적으로 적용하는 것이 원칙입니다.
실무 체크리스트
- 같은 엔티티가 여러 목록에 중복으로 나타나 업데이트 시 여러 곳을 고쳐야 하는 상태가 있는가?
- 그런 데이터가 있다면
createEntityAdapter로 정규화해 O(1) 업데이트 구조로 바꿨는가? - 모든 상태를 습관적으로 정규화하려 하고 있지는 않은가(재사용되지 않는 데이터는 정규화가 불필요할 수 있다)?
- 엔티티의 고유 키가
id가 아니라면selectId를, 항상 특정 순서를 유지해야 한다면sortComparer를 지정했는가?
연습 과제
기초(★☆☆)
- 25편의
posts배열을byId/allIds구조로 손수 정규화해보고, 게시글 하나의 제목을 수정하는 리듀서를 작성해보세요.
중급(★★☆)
createEntityAdapter로postsSlice를 다시 작성하고,addOne/updateOne/removeOne이 실제로 정규화된 구조를 올바르게 갱신하는지 확인해보세요.
고급(★★★)
- 정규화된
posts와users두 엔티티를 조합해, 특정 작성자가 쓴 모든 게시글 제목 목록을 반환하는 메모이제이션 selector(14편의createSelector활용)를 작성해보세요. postsAdapter에sortComparer를 추가해 게시글이 항상 최신순으로 정렬되게 만들고,selectId를 커스터마이징해_id필드를 쓰는 가상의 백엔드 응답을 정규화해보세요.
요약
- 중첩·중복된 배열 구조는 엔티티 하나를 바꿀 때 전체를 순회해야 하는 비효율과 누락 위험을 만든다.
- ID를 키로 쓰는 정규화된 구조는 엔티티 하나의 업데이트를 O(1)로 만들고, 중복 저장을 없앤다.
createEntityAdapter가 정규화된 CRUD 리듀서와 표준 selector를 자동 생성해주며,selectId/sortComparer로 키 필드와 정렬 기준을 커스터마이징할 수 있다.
참고 문헌 및 출처(추천)
- Redux Toolkit 공식 문서, “createEntityAdapter” API 레퍼런스
- Redux 공식 문서, “Normalizing State Shape”
- Redux 공식 문서, “Redux Essentials, Part 6: Performance and Normalizing Data”
![Featured image of post [Redux] 27. 정규화(Normalization) - 복잡한 데이터 관리](/post/redux/normalization/wordcloud_hu_58ad713b488c3d30.webp)
![[Redux] 25. 실습: RTK Query로 블로그 앱 만들기](/post/redux/practice-blog-app/wordcloud_hu_1c4aa9e1677f5dbb.webp)
![[Redux] 26. Redux 프로젝트 구조 - 확장 가능한 설계](/post/redux/redux-project-structure/wordcloud_hu_379134b467bc38d9.webp)
![[Redux] 27. 정규화(Normalization) - 복잡한 데이터 관리](/post/redux/normalization/wordcloud_hu_f70eb8fa31f8f97c.webp)
![[Redux] 28. Redux와 TypeScript - 타입 안전한 상태 관리](/post/redux/redux-typescript/wordcloud_hu_5c9ae286e05acf8c.webp)
![[Redux] 29. Redux 성능 최적화와 디버깅 심화](/post/redux/redux-performance-debugging/wordcloud_hu_d21c809d96be34b8.webp)
![[Redux] 00. 커리큘럼 - 학습 로드맵](/post/redux/getting-started-redux/wordcloud_hu_2ed9b2e29c50fe99.webp)
![[Technology] CRDT(Conflict-Free Replicated Data Types) 개요와 활용](/post/2024-08-29-crdt/wordcloud_hu_a8e3bf4599b75c3c.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)
![[DevOps] GitHub Actions Hugo 빌드: WebP 캐시로 빌드 시간 단축](/post/2026-02-25-github-actions-hugo-webp-cache-build-time/wordcloud_hu_e3555ec4c38572c1.webp)
![[Performance] 지연 시간 메트릭 마스터하기: P90, P95, P99](/post/2024-09-09-latency-metrics/wordcloud_hu_6847a4fbb17f3a81.webp)