본문 바로가기

프로그래밍 공부내용/리액트(React)

Normalizing State Shape(상태를 이쁘게 관리하는 법) - 리덕스 공식문서 읽기

Before Reading

실무에서 어플리케이션들은 복잡하게 얽히고 연결된 데이터를 다뤄야 합니다.

 

예를 들면, 블로그 에디터(ex 티스토리)는 많은 포스트를 가지고, 각각의 포스트는 여러 코멘트를 가지고, 포스트와 코멘트는 또 누가 썼는지를 알아야하고.... 복잡합니다.

 

이런 어플리케이션의 데이터는 아래와 같은 모양일 것입니다.

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
]

데이터의 구조는 복잡하고, 반복됩니다.

 

이것은 아래와 같은 이유에서 문제가 될 수 있습니다.

 

- 데이터가 몇몇 곳 에서 반복되면, 제대로 업데이트 됐는지 확인하기 어렵다.

- 서로 복잡하게 연결된(nested중첩된) 데이터를 다루려면, 더 복잡한 리듀서를 가져야 한다. 이 경우, 깊게 nested 된 영역은 매우 지저분하고 빠르게 퍼질 수 있음

- 불변성을 지키면서 업데이트를 하기 위해서는 State Tree에서 모든 조상을 복사하고 업데이트 해야됨. 새로 생성된 객체는 연결된 ui를 야기함. 이것은 리렌더링으로 이어지고, 데이터(ui에서 보여지는 값)가 변하지 않은 관련없는 ui 컴포넌트들도 리렌더링 하게 됨.

 

이러한 이유 때문에, 리덕스에서는 Store의 일부가 데이터베이스 인 것 처럼 다뤄지고 정규화 하는 것을 추천함

 

 

Desinging a Normalized State

데이터 정규화의 컨셉은 아래와 같음

- 각각의 데이터 타입은 state에서 각각의 table을 가짐

- 각각의 table은 ID를 키, Item을 value로 하는 Item을 각각 저장해야 함

- Item의 ID를 저장함으로써 각각의 item의 참조를 해결할 수 있어야 함

- 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"]
    }
}

개인적으로는 조금 더 이해하기는 힘들어졌고 복잡해졌다고 생각하는데...

쓸때는 이게 좋은가 봅니다. 백엔드를 하지 않고, 간단한 DB수업을 듣긴 했는데

그때 들었던 DB table로 생각하면 조금 이해가 되기는 하네요.

 

이전에는 Post 단위로 Post id, author, body, comment를 가지게 저장했고

 

normalize 이후에는 3개의 카테고리로 쪼개서 User를 따로 저장, Comment를 따로 저장,  Post를 따로 저장했고

각각의 카테고리는 순회하기 쉽도록 all Ids 라는 영역을 가집니다.

 

처음의 구조와 비교했을 때 이 State 구조가 더 Flat 합니다.

왜 더 좋은지, 그 이유는 아래와 같습니다.

- 각각의 item은 한 군데에서만 정의됐기 때문에 여러군데서 수정할 필요없음

- 리듀서의 로직이 깊게 nested 된 것 까지 다룰필요가 없음. 더 간단한 리듀서 로직이 될 것임.

-  특정 Item을 검색하거나 업데이터 하는 로직이 더 간단하고 일관성 있습니다.

어떤 Item인지(User인지 Comment인지, Post인지) 랑 Id만 주어지면, 바로 수정, 검색이 가능합니다.

- 각 Item의 유형이 분리 돼 있어서, Comment의 내용을 변경하거나 하는 경우에 "comment > byId > comment" 부분의 새 복사본만 Tree에서 새 복사본으로 갈아끼워주면 됩니다. 이건 더 적은 UI의 부분만 업데이트 되면 된다는 걸 의미합니다.

반대로 수정 전의 Data형태에서는 Comment object를 통째로 바꿔야하기 때문에 전체 Post Component와 Comment Componnent가 리렌더링 될 것입니다.

 

좀 복잡하지만 아래와 같습니다.

## 이전 구조에서는 변경점을 포함해서 이걸 다 새로 갈아끼워야함 -> 이걸 다 리렌더링 해야 함

{
    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: '.....'
      },
    ]
  }
  
  
  ## 새 구조에서는 Comment만 갈아끼우면 되고 -> 현재 page에서 Comment들만 리렌더링 될 것
  
   byId : {
            "comment1" : {
                id : "comment1",
                author : "user2",
                comment : ".....",
            },
            "comment2" : {
                id : "comment2",
                author : "user3",
                comment : ".....",
            },
        },
        allIds : ["comment1", "comment2"]
    },

normalized 된 상태구조는 많은 구성요소가 연결 될 수 있고, 자신의 data만 바라볼 수 있습니다.

연결이 적은 구성요소는 많은 data를 바라보고, 아래로 전달합니다.

 

결과적으로 부모 구성요소가 자식 요소에게 ID를 전달하는것이 UI 성능을 개선하는데 좋은 패턴입니다.

normalize 해서 쓰십시오!!

 

Organizing Normalized Data in State

전형적인 어플리케이션들은 연관된 데이터와 안 연관된 데이터들이 섞여 있습니다.

어떻게 묶을지 정해진 규칙은 없지만, 일반적인 한가지 방법은 'Table'을 통해서 공통된 parent key를 가지고 통제하는 것입니다. 

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

여러가지 방법으로 확장가능합니다. 

예를 들면, 엔티티를 많이 수정하는 응용프로그램은 '현재'와 '진행중' 에 대한 두가지 테이블을 상태로 유지하도록 할 수 있음.

항목이 편집되면 '진행중'인 작업 섹션에 값을 복사 해 놓고, 값이 수정 될 때마다 '진행중'에 있는 복사본에 적용 돼서,

다른 UI의 부분들은 원래와 같은데 값만 수정할 수 있음.

'Reset'을 누르면 그냥 '진행중' 섹션에서 지우고 원본 data를 다시 current에서 가져와서 '진행중'에 복사하면 됨.

'적용' 할때는 '진행중'에서 'current'로 가져오기만 하면 되고.

 

영미권 사람들이 말을 왜 이렇게 복잡하게 쓰는지... 내가 영어를 못하는지... 어려웠지만 간단하게 블로그 생각하면 됨.

'현재', '진행중' 두가지 상태를 놓고 관리하고 '현재' 는 원본처럼 관리, '진행중'은 임시파일 처럼 관리함.

각각의 기능에서 아래와 같이 동작함.

'수정하기' 버튼을 누르면 '현재'에 있는 값을 '진행중'에 복사한다음 진행중에서만 값을 수정함. 원본 보호 됨.

'리셋'버튼을 누르면 '진행중'을 삭제한다음, '현재'에 남아있던 원본값으로 다시 초기화

'적용'버튼을 누르면 '진행중'에 있는 원본값을 '수정중'으로 업데이트 함.

내 생각이지만 만약에 이전으로 되돌리기 같은 기능도 넣고 싶으면 '이전', '현재', '진행중' 같이 하면 될듯

 

Relationships and Tables

리덕스를 database처럼 다루려고 하고 있기 때문에, 많은 DB에서 적용되는 원칙들이 적용가능하다.

예를들면 다대다 연결이나 이런것들이 있음.

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

        }
    }
}

 

 

Normalizing Nested Data

api가 보내는  data가 nested 돼 있을수도 있기 때문에, normalize 한 다음 저장하는게 좋음.

normalize 랑리브러리 같은 것도 쓸 수 있고,  db 스키마 같은거를 가져다가 쓸 수도 있음.

https://github.com/paularmstrong/normalizr 이런거 쓰면 가능함.(normalizr 라는 라이브러리인듯)

 

 

 

노란글씨는 내 생각, 검은 글씨는 해석한 내용입니다.

영어를 잘 못하지만 해석해 봤습니다.

 

링크

https://redux.js.org/usage/structuring-reducers/normalizing-state-shap