본문 바로가기

Web + APP/Angular

Normalizing State Shape

반응형
SMALL

안녕하세요. 꼬동입니다.

 

직역을 하자면, State의 형태를 표준화한다. 라는 뜻이 될텐데, 어떤 의미를 가지는지 한 번 같이 보도록 합시다.

 

해당 글은 Redux 로직과 패턴을 분석하는 글로서, Redux 공홈 Docs를 옮겨 적는 글입니다.

 

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


대부분의 애플리케이션은 객체에서 depths를 가진 data를 많이 표현하고 있습니다.

 

블로그를 예시로 든다면, 글과 글쓴이 => 댓글들

 

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
]

이런 구조 아마 많이 봤을텐데요.

 

요런 구조는 쉽게 복잡해질 수 있는 구조이기도 하며, 반복도 많죠.

 

그렇기에 아래와 같은 걱정들이 발생합니다.

 

  • 데이터들이 여러 곳에서 중복이되며, 적절하게 update를 하기가 힘들어집니다.
  • 깊은 depth를 가진 data를 가지게 되면, reducer logic이 금방 복잡해지며, 이는 코드의 복잡성을 야기합니다.
  • immutable data를 update할 때, 새로운 object에 모든 tree의 자식 노드들이 복사가 되어야하는데, depth를 가진 object state들은 깊은 복사를 필요로 하기 때문에, 이는 전체적으로 관련이 없는 UI components가 리렌더링되는, side-effect가 발생할 수 있습니다.

 

이러한 이유 때문에, Redux store에서 depth와 관계가 있는 data를 가지고 놀 땐, DB에 넣는거 처럼 nomalized form을 유지하는게 중요합니다.

 

Normalized State 디자인

주된 컨셉은 아래와 같습니다.

 

  • data의 각 타입들은 state에서 table을 가집니다.
  • 각, data table들이 object 안에 각각의 item들로 저장이 되는데, item의 id들을 키로서 그리고 items들은 그 자체로 values로서 가집니다.
  • 각각의 items 들의 참조는 저장된 item의 ID로 결정됩니다.
  • ID의 배열은 정렬이됩니다.

 

이렇게 하면 아래와 같은 shape을 가지겠죠.

 

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

 

이러한 state 구조는 더 다루기가 쉬워집니다. 그리고, nested format과 비교를 했을 때, 여러가지 좋은 효과를 볼 수 있는데,

 

  • 각, item은 하나의 place에서만 정의가 되기 때문에, item이 update가 되더라도, 여러 값들을 바꿀 필요가 없어집니다.
  • reducer logic이 deep level object를 다룰 필요가 없어져서 좀 더 단순하게 바뀝니다.
  • 주어진 item의 검색 / 수정 로직이 일관성이 생기고 단순해집니다. 주어진 아이템의 타입과 ID만 있으면, 간단한 step으로 값을 꺼내올 수 있습니다.
  • 각 data type이 분리되어 있기 때문에, comment를 수정한다고 예시를 들면, "comments > byId > comment" 만 tree를 따라 수정을 거치면 됩니다. 이 말은 즉슨 UI에서 정말 필요로 하는 부분만 바뀔 수 있다는 것인데, 반대로 기존에 했던대로 사용을 한다면, comment를 수정하면 그 부모의 object도 수정될 것이고, 모든 objects의 배열이 수정되면서 상관 없는 부분들도 re-render가 되기 때문에, 먼저 설명한 방법이 더 효율적일 수 있습니다.

 

normalized state 구조는 일반적으로 component가 그들만의 데이터를 바라보기 위해 사용됩니다. 물론, component끼리 연결된 경우도 있을 건데요. 이런 경우에 어떻게 조직화 할건지 알아봅시다.

 

State 에서 Normalized Data를 조직화 해보기

전형적인 어플리케이션은 관계형 데이터와 비관계형 데이터가 섞여있습니다.

 

서로 다른 data 형식들이 어떻게 구성되어야 하는지 정확한 규칙은 없지만, 하나의 "entities" 공통적인 부모의 키를 아래에두고 관계형 "tables"를 두는 것입니다.

 

위와 같은 방법으로 state structure를 만들어보면, 아래와 같습니다.

 

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

 

이러한 방법은 무수히 확장될 수 있습니다.

 

예를 들어, entities의 수정이 많은 애플리케이션의 경우 하나의 state에 여러 table을 두기를 원할 텐데요. "current" item values와 "work-in-progress" item values를 둘 수 있겠죠.

 

item이 수정될 때 "work-in-progress" section에 복사가된다고 생각해봅시다. 그리고 업데이트는 작업에서, "work-in-progress"에 복사본이 적용되어, UI의 다른 부분이 여전히 원본 버전을 참조하는 동안 해당 데이터 집합에 의해 편집 양식을 제어할 수 있습니다.

 

편집 양식을 "Resetting"을 하기 위해선 단순히 "work-in-progress"를 제거하고 다시 "current"의 값을 "work-in-progress"에 복사를 하면 되곘죠.

 

반면, "applying"을 한다면, "work-in-progress"를 "current"에 포함시키면 되는 이러한 방법이 만들어질 수 있습니다.

 

관계와 Tables

Redux store를 "database"로 대하고 사용하기 때문에, 많은 database 원칙들이 여기에도 사용될 수 있습니다.

 

예를들어 Many To Many 관계를 가졌다고 생각을 해봅시다. 그럼 그 두 테이블을 연결짓는 테이블이 하나 더 필요하겠죠 ?

 

아마 아래와 같이 사용이 될 것입니다.

 

흔하죠 ?

 

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

 

만약, 특정 작가의 책들을 모두 보고 싶다고 한다면, table을 한 번 loop 하면 끝입니다. 클라이언트 앱에서 주어진 데이터의 양과 JS engines의 속도면 충분히 좋은 performance를 뽑아낼 수 있겠죠.

 

Normalizing Nested Data

APIs가 nested form을 보내기 때문에, state tree에 적용시키기 위해 data를 normalized shape으로 변형을 시켜야 할 경우가 있을 것입니다.

 

Normalizr library의 경우 이 일을 대신할 수 있을 것인데요. 관계, 스키마를 정의하고 Normalizr에게 응답 값을 주면 얘들이 알아서 응답 값을 촤르륵 바꿔줄 것인데요. 이 결과값으로 action에 추가하고 store에 update할 수 있겠죠.

 


 

해당 글을 번역하면서 느낀 점은 요 Redux State Structure가 굉장히 재밌네요.

 

뭔가 백엔드 테이블 작업을 하는 듯한 느낌을 씨게 받았습니다.

 

한 번 예제를 만들어보는 것도 나쁘지 않을거 같아요.

 


이상 Normalizing State Shape였습니다. ^_^

반응형
LIST

'Web + APP > Angular' 카테고리의 다른 글

constructor과 ngOnInit의 차이는 ?  (0) 2022.03.15
NGRX - Selectors  (0) 2021.11.26
NGRX - Reducers  (0) 2021.10.31
NGRX - Actions  (0) 2021.10.24
Angular : 같은 URL에서 Refetch data  (2) 2021.10.17