이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →
상태 형태 정규화
많은 애플리케이션은 중첩되거나 관계형 데이터를 다룹니다. 예를 들어 블로그 편집기는 여러 개의 포스트를 가질 수 있으며, 각 포스트는 여러 개의 댓글을 가질 수 있고, 포스트와 댓글 모두 사용자가 작성합니다. 이런 애플리케이션의 데이터 구조는 다음과 같을 수 있습니다:
const blogPosts = [
{
id: 'post1',
author: { username: 'user1', name: 'User 1' },
body: '......',
comments: [
{
id: 'comment1',
author: { username: 'user2', name: 'User 2' },
comment: '.....'
},
{
id: 'comment2',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
},
{
id: 'post2',
author: { username: 'user2', name: 'User 2' },
body: '......',
comments: [
{
id: 'comment3',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
},
{
id: 'comment4',
author: { username: 'user1', name: 'User 1' },
comment: '.....'
},
{
id: 'comment5',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
}
// and repeat many times
]
데이터 구조가 다소 복잡하고 일부 데이터가 반복된다는 점에 주목하세요. 이는 다음과 같은 여러 이유로 문제가 됩니다:
-
데이터 조각이 여러 곳에 중복될 경우 적절하게 업데이트하기 어려워집니다.
-
중첩된 데이터는 해당 리듀서 로직이 더 깊게 중첩되어 복잡해져야 함을 의미합니다. 특히 깊게 중첩된 필드를 업데이트하려고 하면 매우 빠르게 복잡해질 수 있습니다.
-
불변성(immutable) 데이터 업데이트는 상태 트리의 모든 상위 요소를 복사하고 업데이트해야 하며, 새로운 객체 참조는 연결된 UI 컴포넌트의 재렌더링을 유발합니다. 따라서 깊게 중첩된 데이터 객체를 업데이트하면 표시하는 데이터가 실제로 변경되지 않았더라도 전혀 관련 없는 UI 컴포넌트가 강제로 재렌더링될 수 있습니다.
이 때문에 Redux 스토어에서 관계형 또는 중첩된 데이터를 관리하는 권장 접근 방식은 스토어의 일부를 데이터베이스처럼 취급하고 해당 데이터를 정규화된(normalized) 형태로 유지하는 것입니다.
정규화된 상태 설계하기
데이터 정규화의 기본 개념은 다음과 같습니다:
-
각 데이터 유형은 상태 내 자체 "테이블"을 가집니다.
-
각 "데이터 테이블"은 개별 항목을 객체로 저장하며, 항목의 ID를 키로, 항목 자체를 값으로 사용해야 합니다.
-
개별 항목에 대한 참조는 항목의 ID를 저장하는 방식으로 처리해야 합니다.
-
정렬 순서를 나타내기 위해 ID 배열을 사용해야 합니다.
위 블로그 예시에 대한 정규화된 상태 구조의 예시는 다음과 같을 수 있습니다:
{
posts: {
byId: {
post1: {
id: "post1",
author: "user1",
body: "......",
comments: ["comment1", "comment2"]
},
post2: {
id: "post2",
author: "user2",
body: "......",
comments: ["comment3", "comment4", "comment5"]
}
},
allIds: ["post1", "post2"]
},
comments: {
byId: {
comment1: {
id: "comment1",
author: "user2",
comment: "....."
},
comment2: {
id: "comment2",
author: "user3",
comment: "....."
},
comment3: {
id: "comment3",
author: "user3",
comment: "....."
},
comment4: {
id: "comment4",
author: "user1",
comment: "....."
},
comment5: {
id: "comment5",
author: "user3",
comment: "....."
}
},
allIds: ["comment1", "comment2", "comment3", "comment4", "comment5"]
},
users: {
byId: {
user1: {
username: "user1",
name: "User 1"
},
user2: {
username: "user2",
name: "User 2"
},
user3: {
username: "user3",
name: "User 3"
}
},
allIds: ["user1", "user2", "user3"]
}
}
이 상태 구조는 전체적으로 훨씬 평탄합니다. 원본 중첩 형식과 비교할 때 다음과 같은 측면에서 개선됩니다:
-
각 항목이 한 곳에서만 정의되므로 해당 항목이 업데이트될 때 여러 곳을 동시에 수정하려고 시도할 필요가 없습니다.
-
리듀서 로직이 깊은 수준의 중첩을 처리할 필요가 없으므로 훨씬 단순해질 수 있습니다.
-
특정 항목을 검색하거나 업데이트하는 로직이 이제 상당히 단순하고 일관적입니다. 항목 유형과 ID가 주어지면 다른 객체를 파헤치지 않고도 몇 단계만에 직접 조회할 수 있습니다.
-
각 데이터 유형이 분리되어 있으므로 댓글 텍스트 변경과 같은 업데이트는 "comments > byId > comment" 트리 부분만 새로 복사하면 됩니다. 이는 일반적으로 데이터 변경으로 인해 업데이트해야 하는 UI 부분이 줄어듦을 의미합니다. 반면 원본 중첩 형태에서 댓글을 업데이트하려면 댓글 객체, 상위 포스트 객체, 모든 포스트 객체 배열을 업데이트해야 하며, UI의 모든 포스트 컴포넌트와 댓글 컴포넌트가 재렌더링될 가능성이 있습니다.
정규화된 상태 구조는 일반적으로 더 많은 컴포넌트가 연결(connect)되고 각 컴포넌트가 자체 데이터를 조회할 책임이 있음을 의미합니다. 소수의 연결된 컴포넌트가 대량의 데이터를 조회하고 모든 데이터를 하위로 전달하는 방식과는 대조적입니다. 연결된 부모 컴포넌트가 단순히 항목 ID를 연결된 자식 컴포넌트에 전달하는 것은 React Redux 애플리케이션에서 UI 성능을 최적화하는 좋은 패턴이며, 따라서 상태를 정규화된 형태로 유지하는 것은 성능 개선에 핵심적인 역할을 합니다.
상태 내 정규화된 데이터 구성하기
일반적인 애플리케이션은 관계형 데이터와 비관계형 데이터가 혼합되어 있을 가능성이 높습니다. 서로 다른 유형의 데이터를 정확히 어떻게 구성해야 하는지에 대한 단일 규칙은 없지만, 일반적인 패턴은 관계형 "테이블"을 "entities"와 같은 공통 상위 키 아래에 배치하는 것입니다. 이 접근 방식을 사용하는 상태 구조는 다음과 같을 수 있습니다:
{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities: {
entityType1 : {....},
entityType2 : {....}
},
ui: {
uiSection1 : {....},
uiSection2 : {....}
}
}
이를 여러 방식으로 확장할 수 있습니다. 예를 들어 엔티티 편집이 빈번한 애플리케이션은 상태에 두 세트의 "테이블"을 유지할 수 있습니다. 하나는 "현재(current)" 항목 값용이고 다른 하나는 "작업 중(work-in-progress)" 항목 값용입니다. 항목이 편집되면 해당 값이 "작업 중" 섹션으로 복사되며, 업데이트 작업은 모두 "작업 중" 복사본에 적용됩니다. 이렇게 하면 편집 폼이 해당 데이터 세트로 제어되는 동시에 UI의 다른 부분은 원본 버전을 참조할 수 있습니다. 편집 폼 "초기화(resetting)"는 단순히 "작업 중" 섹션에서 항목을 제거하고 "현재"에서 원본 데이터를 "작업 중"으로 재복사하는 것이며, 편집 내용 "적용(applying)"은 "작업 중" 섹션의 값을 "현재" 섹션으로 복사하는 과정입니다.
관계와 테이블
Redux 저장소의 일부를 "데이터베이스"로 취급하기 때문에 데이터베이스 설계 원칙 대부분이 여기에도 적용됩니다. 예를 들어 다대다 관계가 있는 경우 해당 항목의 ID를 저장하는 중간 테이블(일반적으로 "조인 테이블(join table)" 또는 "연관 테이블(associative table)"이라고 함)을 사용해 모델링할 수 있습니다. 일관성을 위해 실제 항목 테이블에 사용한 것과 동일한 byId 및 allIds 접근 방식을 적용할 수도 있습니다.
{
entities: {
authors: {
byId: {},
allIds: []
},
books: {
byId: {},
allIds: []
},
authorBook: {
byId: {
1: {
id: 1,
authorId: 5,
bookId: 22
},
2: {
id: 2,
authorId: 5,
bookId: 15
},
3: {
id: 3,
authorId: 42,
bookId: 12
}
},
allIds: [1, 2, 3]
}
}
}
"이 저자가 쓴 모든 책 조회(Look up all books by this author)"와 같은 작업은 조인 테이블을 한 번 순회하는 것만으로 쉽게 수행할 수 있습니다. 클라이언트 애플리케이션의 일반적인 데이터 양과 JavaScript 엔진의 속도를 고려할 때, 이러한 작업은 대부분의 사용 사례에서 충분히 빠른 성능을 제공할 것입니다.
중첩된 데이터 정규화
API는 종종 중첩된 형태로 데이터를 반환하므로, 해당 데이터를 상태 트리에 포함시키기 전에 정규화된 형태로 변환해야 합니다. 일반적으로 Normalizr 라이브러리가 이 작업에 사용됩니다. 스키마 유형과 관계를 정의한 후 스키마와 응답 데이터를 Normalizr에 제공하면 정규화된 변환 결과를 출력합니다. 이 출력은 액션에 포함되어 저장소를 업데이트하는 데 사용할 수 있습니다. 사용법에 대한 자세한 내용은 Normalizr 문서를 참조하세요.