회사에서 최근본 / 찜목록 상품에 대해 추가/삭제 기능을 가진 페이지네이션 페이지를 작업했었다. 이 때, 서버에서 내려주는 데이터가 중첩되고 복잡하고 그대로 아무생각없이 그대로 사용하게 됐을 때, 데이터 구조를 개선할 수 없을까라는 생각에 찾은 포스팅이 있다. 이 포스팅을 읽고 나서 state 구조 개선에 큰 도움을 얻어서 번역을 블로그에 직접 작성해보았다.
중첩된 데이터에 대한 처리. 블로그 에디터는 많은 포스트와 해당 포스트는 많은 댓글을 가질 수 있다. 이 포스트와 댓글은 유저에 의해 작성된다. 아래 예제 데이터를 보자.
const blogPosts = [
{
id: "post1",
author: {username: "user1", name:"u1"},
body: ";..",
commnets: [
{
id: "comment1",
author: {..},
comment: ".."
},
{
...
}
]
},
{
id: "post2",
...
}
]
위 데이터 구조는 꽤 복잡하고 몇몇 데이터는 반복된다. 이는 몇 가지 이유에서 우려된다.
위와 같은 이유 때문에, 스토어의 중첩되거나 연관된 데이터 처리시 스토어를 데이터베이스의 일부처럼 정규화된 형태로 유지하는 방법을 권장한다.
정규화
{
posts : {
byId : {
"post1" : {
id : "post1",
author : "user1",
body : "......",
comments : ["comment1", "comment2"]
},
...
}
allIds : ["post1", "post2"]
},
comments : {
byId : {
"comment1" : {
id : "comment1",
author : "user2",
comment : ".....",
},
"comment2" : {
id : "comment2",
author : "user3",
comment : ".....",
},
...
},
allIds : ["comment1", "comment2", "comment3", "commment4", "comment5"]
},
users : {
byId : {
"user1" : {
username : "user1",
name : "User 1",
},
...
},
allIds : ["user1", "user2", "user3"]
}
}
첫 코드와 비교해서 구조가 전체적으로 flat하다. 초기 구조에 비해 개선된 점은
normalized state 구조는 일반적으로 더 많은 컴포넌트에 연결되고, 각 컴포넌트는 자기가 가진 데이터에 대해서만 책임을 갖는다. 반대로 기존의 중첩된 데이터 구조의 경우, 극소수의 connected components들이 엄청 큰 데이터에 접근하고 그 데이터를 아래(하위 컴포넌트)로 전달해야한다.
연결된 부모 컴포넌트를 여러개 가져서, 단순히 아이템의 IDs를 연결된 자식 컴포넌트에 전달하도록 하는것
이것은 React Redux application에서 UI 성능을 최적화하는 좋은 패턴이다. 그래서 state를 normalized하게 유지하는 것은 퍼포먼스를 개선하는 핵심이다.
{
simpleDomainData1: {....},
simpleDomainData2: {....}
entities : {
entityType1 : {....},
entityType2 : {....}
}
ui : {
uiSection1 : {....},
uiSection2 : {....}
}
}
관계있는 데이터와 그렇지 않은 데이터가 공존. 다른 데이터 타입이 어떻게 구성되어야 하는지에 대한 규칙이 정확히 하나만 있는것이 아니다. 하지만, 이 중 하나는 관계있는 테이블을 entities
와 같은 일반적인 부모 키 아래에 넣는 패턴이다.
위 예제는 여러가지 방법으로 확장될 수 있다. 예를 들어 많은 수정이 일어나는 애플리케이션은 2가지 상태 “테이블”을 유지하고 싶을거다. “현재”항목 값과 “진행단계” 항목 값이다. UI의 다른 부분이 원래 버전을 참조하는 동안 해당 데이터로 편집 폼을 제어할 수 있다. 편집 폼을 재지정하는 것은 그저 “진행 단계”의 항목을 지우고 “현재”섹션의 이전 데이터를 “진행단계”로 다시 복사하기만 하면 된다. 편집을 “적용”하는 동안 “진행 단계”섹션의 값을 “현재”섹션으로 복사해야 한다.
왜냐하면 Redux store를 database
로 다루기 때문에, database 설게의 많은 원칙이 동일하게 적용된다.
예컨대, 만약 다대다 관게를 가진다면, 각 테이블의 기본키 ID를 저장하는 연결 테이블
을 모델링 할 수 있다.(join table
or associatvie tabl
로 알려져있다). 일관성을 위헤, 실제 테이블에 사용한 것과 동일한 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
와 같은 operation은 join table의 single loop으로 쉽게 가져올 수 있다.
API는 데이터를 nested form으로 보내기 때문에, state tree에 포함되기 전에 normalized shape으로 변경되야 한다. 스키마 타입과 relation을 정의하고, Normalizr에게 response data와 schema를 전달하여 정규화된 응답 데이터를 얻을 수 있다.