본문 바로가기

FrontEnd

상태 정규화하기[Redux][Normalizing State Shape][프론트엔드 상태관리]

반응형

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

 

Normalizing State Shape | Redux

Structuring Reducers > Normalizing State Shape: Why and how to store data items for lookup based on ID

redux.js.org

많은 애플리케이션은 중첩되거나 관계형인 데이터를 처리합니다.
블로그 편집자는 많은 게시물을 가질 수 있고,
각 게시물에는 많은 댓글이 있을 수 있으며, 게시물과 댓글은 모두 유저가 작성합니다.
애플리케이션 데이터는 다음과 같을 수 있습니다.
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
]
데이터 구조가 복잡하고 일부 데이터가 반복됩니다. 이것은 여러 가지 이유로 우려됩니다.
  • 데이터 조각이 여러 곳에 중복되면 적절하게 업데이트되었는지 확인하기가 더 어려워집니다.
  • 중첩 데이터는 해당 리듀서 로직이 더 중첩되어야 하므로 더 복잡해야 함을 의미합니다.
    • 특히, 깊게 중첩된 필드를 업데이트하려고 하면 로직은 더욱 복잡해집니다.
  • 불변 데이터 업데이트는
    • 상태 트리의 모든 조상도 복사, 업데이트해야 하고,
    • 이 새 객체 참조로 인해 연결된 UI 컴포넌트가 다시 렌더링되기 때문에
    • 깊이 중첩된 데이터 개체에 대한 업데이트는 해당 업데이트와 관련이 없는 UI 컴포넌트를 강제로 다시 렌더링할 수 있습니다.
    • 즉, 표시하는 데이터가 실제로 변경되지 않은 경우에도 렌더링합니다.

Redux 스토어에서 관계형 또는 중첩 데이터를 관리하는 데 권장되는 접근 방식은

  • 저장소의 일부를 마치 데이터베이스처럼 처리하고
  • 해당 데이터를 정규화된 형식으로 유지하는 것입니다.

정규화 상태 디자인

데이터 정규화의 기본 개념은 다음과 같습니다.
  1. 각 데이터 타입은 상태에서 고유한 "테이블"을 가져옵니다.
  2. 각 "데이터 테이블"은 항목의 ID를 키로, 항목 자체를 값으로 사용하여 객체(테이블)에 개별 항목을 저장해야 합니다.
  3. 개별 항목에 대한 참조는 항목의 ID를 저장하여 수행해야 합니다.
  4. 순서를 나타내기 위해 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의 모든 게시물 컴포넌트와 댓글 컴포넌트가 다시 렌더링되었을 수 있습니다.
정규화된 상태 구조는
일반적으로 더 많은 컴포넌트가 연결되어 있고  각 컴포넌트가 자체 데이터를 조회하는 역할을 하는 반면,
(주 : 각 ListItem의 컨테이너 컴포넌트를 의미함)
연결된 소수의 컴포넌트가 많은 양의 데이터를 조회하고 해당 데이터를 모두 아래로 전달한다는 것을 의미합니다.
(주 : List 컨테이너 컴포넌트를 의미함)
결과적으로 연결된 부모 컴포넌트가 연결된 자식에게 항목 ID를 전달하는 것은
React Redux 애플리케이션에서 UI 성능을 최적화하는 좋은 패턴이므로
상태를 정규화하여 유지하는 것이 성능 향상에 중요한 역할을 합니다.


상태 내에서 정규화된 데이터 / 비졍규화 데이터 정돈하기

일반적인 애플리케이션에는 관계형 데이터와 비관계형 데이터가 혼합되어 있을 수 있습니다.
서로 다른 타입의 데이터가 어떻게 구성되어야 하는지에 대한 단일 규칙은 없지만
한 가지 일반적인 패턴은 "엔티티"와 같은 공통 상위 키 아래에 관계형 "테이블"을 배치하는 것입니다.
이 접근 방식을 사용하는 상태 구조는 다음과 같습니다.
{
    simpleDomainData1: {....},
    simpleDomainData2: {....},
    entities : {
        entityType1 : {....},
        entityType2 : {....}
    },
    ui : {
        uiSection1 : {....},
        uiSection2 : {....}
    }
}​
이것은 여러 가지 방법으로 확장될 수 있습니다.
예를 들어, 엔터티를 많이 편집하는 애플리케이션은 "current" 항목 값과 "wop" 항목 값에 대한 두 세트의 "테이블"을 상태로 유지하기를 원할 수 있습니다.
항목이 편집될 때 해당 값은 "wop" 섹션에 복사될 수 있으며,
이를 업데이트하는 모든 작업은 "wop" 복사본에 적용됩니다.
예를 들어, form은 해당 데이터를 참조하며 수정하고, 업데이트 시 current 항목에 wop 항목을 복사합니다.
혹은 reset하면 current 값을 wop 항목에 복사합니다.
이 작업 동안 다른 UI 컴포넌트는 current를 참조하여 해당 수정의 영향을 받지 않습니다.

관계와 테이블

Redux 저장소의 일부를 "데이터베이스"로 취급하기 때문에 데이터베이스 설계의 많은 원칙이 여기에도 적용됩니다.
예를 들어 다대다 관계가 있는 경우 해당 항목의 ID를 저장하는 중간 테이블(종종 "조인 테이블" 또는 "연관 테이블"이라고도 함)을 사용하여 이를 모델링할 수 있습니다.
일관성을 위해 다음과 같이 실제 항목 테이블에 사용한 것과 동일한 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]

        }
    }
}​
"이 저자의 모든 책 조회"와 같은 작업은 조인 테이블에 대한 단일 루프로 쉽게 수행할 수 있습니다.
클라이언트 응용 프로그램의 일반적인 데이터 양과 Javascript 엔진의 속도를 감안할 때
이러한 종류의 작업은 대부분의 사용 사례에서 충분히 빠른 성능을 보일 수 있습니다.


중첩된 데이터 정규화

API는 종종 중첩된 형태로 데이터를 다시 보내기 때문에 해당 데이터는 상태 트리에 포함되기 전에 정규화된 형태로 변환되어야 합니다. Normalizr 라이브러리는 일반적으로 이 작업에 사용됩니다.
스키마 유형과 관계를 정의하고 스키마와 응답 데이터를 Normalizr에 제공하면 응답의 정규화된 변환이 출력됩니다.
그런 다음 해당 출력을 작업에 포함하고 저장소를 업데이트하는 데 사용할 수 있습니다.
사용법에 대한 자세한 내용은 Normalizr 문서를 참조하세요.
반응형