Normalizing State Shape


Redux 처음 공부하며 번역했던 글이 있어서 포스팅합니다.

Normalizing State Shape

Normalizing State Shape

대부분 웹 application의 데이터는 중첩되어 있거나, 상관관계를 가지고 있습니다. 예로, 블로그 포스트를 들 수 있습니다. 블로그에는 많은 포스트가 있을 수 있고, 각 포스트는 다수의 코멘트가 달릴 수 있습니다. 그리고 포스트와 코멘트는 유저에 의해 작성됩니다. 이러한 서비스의 데이터는 아래와 같을 수 있습니다.

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
];

데이터의 구조가 복잡하고, 일부 데이터는 중복되어 있습니다. 이러한 구조는 아래와 같은 이유로 문제가 될 수 있습니다.

  • 데이터 일부가 여러 곳에 중복되어 있다면, 데이터를 업데이트 했을때 알맞게 업데이트가 된 것인지 확신하기가 어렵습니다.
  • 중첩된 데이터를 다루기 위해서는 Reducer의 로직 또한 중첩되고 복잡해 져야 합니다. 이러한 작업은 쉽게 매우 까다로운 작업이 될 수 있습니다.
  • immutable data를 업데이틑 하는 것은 state tree의 모든 상위 구조 데이터를 복사하고 그것에 다시 업데이트를 해야합니다. 이러한 과정에서 생기는 새로운 객체의 참조값은 렌더링을 다시하게 합니다. 깊이 중첩된 데이터는 전혀 상관이 없는 UI 컴포넌트를 다시 렌더링하게 만들 수도 있습니다. 실제 데이터의 변화가 없는 경우에도 말입니다.

따라서, 데이터를 다룰 때 전달 받은 데이터를 데이터베이스로 간주하고, 그 데이터를 정규화된 구조로 보관하는 것이 추천됩니다.

Designing a Normalized State

데이터 정규화의 기본 컨셉은 아래와 같습니다.

  • 각 데이터는 state 상 고유의 'table'을 가집니다.
  • 각 'data table'은 개별 아이템을 객체로 저장하고. 각 아이템의 'ID'를 key 값으로 하고, 아이템 자체를 value로 가집니다.
  • 개별 데이터에 접근을 위해 item 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", "commment4", "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"]
    }
}

위의 자료구조가 raw data에 비해 훨씬 flatter 합니다. 이러한 방식은 아래와 같은 장점을 가져옵니다.

  • 각 아이템은 한 곳에만 정의돼 있기 떄문에, 업데이트를 하기 위해 여러곳을 수정할 필요가 없습니다.
  • Reducer가 깊이 중첩된 자료를 다룰 필요가 없기 때문에, 좀 더 단순화 할 수 있습니다.
  • 데이터를 가져오고 수정하는 일이 훨씨 간단해 집니다. 아이템의 타입과 ID가 있으면, 각 데이터에 접근해 데이터를 읽거나 수정하는 것이 더 수월해집니다.
  • 데이터가 쪼개져 있기 때문에, comment 업데이트를 위해 comment > byid > comment 의 부분만 필요로 합니다. 다시 말해서, UI의 적은 부분만 데이터 변경에 따라 수정을 할 수 있습니다.

반면, raw 데이터에서 comment 수정을 위해서는, comment 객체, 부모 post 객체, 전체 posts의 배열이 필요하게 되며, 이는 Post components, Comment component의 리렌더를 초래할 수 있습니다.

Organizing Normalized Data in State

일반적인 application은 관계형 데이터와 비관계형 데이터가 혼재해 있습니다. 다른 데이터 타입을 정리하는 특별한 룰은 없지만, 일반적인 패턴은 있습니다. 그 중 하나는 관계형 table을 common parent key 아래에 두는 것입니다. 아래는 이 패턴의 예시입니다.

{
    simpleDomainData1: {....},
    simpleDomainData2: {....},
    entities : {
        entityType1 : {....},
        entityType2 : {....}
    },
    ui : {
        uiSection1 : {....},
        uiSection2 : {....}
    }
}

entities에 수정을 많이 하는 application의 경우, state에 두 개의 'table'을 둘 수 있습니다. 만약 item이 수정된다면, 그 값을 work-in-progress로 이동 시킵니다. 그리고 아이템이 수정되면, 수정된 값으로 원본 데이터를 교체하는 것입니다.

Relationships and Tables

Redux store의 부분을 데이터베이스의 일부로 이용하기 때문에, 우리는 database design을 적용할 수 있습니다. 예를 들어, many-to-many relationship이라고 한다면, 우리는 intermediate table(a.k.a joint 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]

        }
    }
}

저자별 책을 찾는 작업을 한다면, joint table에서 single loop으로 쉽게 처리할 수 있습니다.