본문 바로가기

FrontEnd

정규화된 상태 업데이트하기 [Managing Normalized Data][Redux][프론트엔드 상태관리]

반응형

원문 : 정규화된 데이터 업데이트

 

상태 정규화에서 언급했듯이 Normalizing State Shape 라이브러리는 중첩된 응답 데이터를 저장소에 통합하기에 적합한 정규화된 형태로 변환하는 데 자주 사용됩니다.
그러나 다른 곳에서 사용되는 정규화된 데이터에 대한 추가 업데이트를 실행하는 문제는 해결되지 않습니다.
자신의 선호도에 따라 다양한 접근 방식을 사용할 수 있습니다. 게시물의 댓글에 대한 변이를 처리하는 예를 사용합니다.
 

일반적인 접근 방식 (다른 상태 관련 라이브러리 없이 구현하기)

단순 병합

한 가지 접근 방식은 액션 내용을 기존 상태로 병합하는 것입니다.
저장된 항목을 업데이트하기 위해 항목의 일부가 있는 액션을 허용할 수 있습니다.
Lodash merge 함수는 우리를 위해 이 일을 합니다. (재귀, 깊은 복사 처리)
import merge from 'lodash/merge'

function commentsById(state = {}, action) {
  switch (action.type) {
    default: {
      if (action.entities && action.entities.comments) {
        return merge({}, state, action.entities.comments.byId)
      }
      return state
    }
  }
}​

 

리듀서 측에서 가장 적은 양의 작업이 필요하지만
액션 생성자는 액션이 전달되기 전에 데이터를 올바른 모양으로 구성하기 위해 잠재적으로 상당한 양의 작업을 수행해야 합니다.
또한 항목 삭제 시도를 처리하지 않습니다.


슬라이스 리듀서 합성

(...프라이빗 리듀서 이용)

슬라이스 리듀서의 중첩 트리가 있는 경우 각 슬라이스 리듀서는 이 작업에 적절하게 응답하는 방법을 알아야 합니다.
  • 액션에 모든 관련 데이터를 포함해야 합니다.
  • 댓글의 ID로 올바른 Post 개체를 업데이트하고
  • 해당 ID를 키로 사용하여 새 댓글 개체를 만들고
  • 모든 댓글 ID 목록에 댓글의 ID를 포함해야 합니다.
이를 위해 슬라이스가 어떻게 맞물릴 수 있는지 봅시다.
(주 : 리듀서의 위임 구조와 export되는 리듀서, 아닌 리듀서를 파악해봅니다.)
  • postsById 슬라이스 리듀서는 이 경우에 대한 작업을 자신만의 reducer인 addComment에 위임합니다.
    • addComment는 새 댓글의 ID를 올바른 게시물 항목에 삽입합니다.
  • commentById 및 allComments 슬라이스 리듀서는 모두 Comments 조회 테이블과 모든 Comment ID 목록을 적절하게 업데이트하는 자신들 만의 리듀서를 가지고 있습니다.
// actions.js
function addComment(postId, commentText) {
  // Generate a unique ID for this comment
  const commentId = generateId('comment')

  return {
    type: 'ADD_COMMENT',
    payload: {
      postId,
      commentId,
      commentText
    }
  }
}

// reducers/posts.js
function addComment(state, action) {
  const { payload } = action
  const { postId, commentId } = payload

  // Look up the correct post, to simplify the rest of the code
  const post = state[postId]

  return {
    ...state,
    // Update our Post object with a new "comments" array
    [postId]: {
      ...post,
      comments: post.comments.concat(commentId)
    }
  }
}

function postsById(state = {}, action) {
  switch (action.type) {
    case 'ADD_COMMENT':
      return addComment(state, action)
    default:
      return state
  }
}

function allPosts(state = [], action) {
  // omitted - no work to be done for this example
}

const postsReducer = combineReducers({
  byId: postsById,
  allIds: allPosts
})

// reducers/comments.js
function addCommentEntry(state, action) {
  const { payload } = action
  const { commentId, commentText } = payload

  // Create our new Comment object
  const comment = { id: commentId, text: commentText }

  // Insert the new Comment object into the updated lookup table
  return {
    ...state,
    [commentId]: comment
  }
}

function commentsById(state = {}, action) {
  switch (action.type) {
    case 'ADD_COMMENT':
      return addCommentEntry(state, action)
    default:
      return state
  }
}

function addCommentId(state, action) {
  const { payload } = action
  const { commentId } = payload
  // Just append the new Comment's ID to the list of all IDs
  return state.concat(commentId)
}

function allComments(state = [], action) {
  switch (action.type) {
    case 'ADD_COMMENT':
      return addCommentId(state, action)
    default:
      return state
  }
}

const commentsReducer = combineReducers({
  byId: commentsById,
  allIds: allComments
})​

다른 접근 방법

Task-Based Updates

리듀서는 함수일 뿐이므로 이 논리를 분할하는 방법은 무한합니다.
슬라이스 리듀서를 사용하는 것이 가장 일반적이지만 보다 액션(Task) 지향적인 구조로 동작을 구성하는 것도 가능합니다.
이것은 종종 더 많은 중첩 업데이트를 포함하므로 dot-prop-immutable 또는 object-path-immutable과 같은 변경할 수 없는 업데이트 유틸리티 라이브러리를 사용하여 업데이트 문을 단순화할 수 있습니다. 다음은 다음과 같은 예입니다.
import posts from "./postsReducer";
import comments from "./commentsReducer";
import dotProp from "dot-prop-immutable";
import {combineReducers} from "redux";
import reduceReducers from "reduce-reducers";

const combinedReducer = combineReducers({
    posts,
    comments
});


function addComment(state, action) {
    const {payload} = action;
    const {postId, commentId, commentText} = payload;

    // State here is the entire combined state
    const updatedWithPostState = dotProp.set(
        state,
        `posts.byId.${postId}.comments`,
        comments => comments.concat(commentId)
    );

    const updatedWithCommentsTable = dotProp.set(
        updatedWithPostState,
        `comments.byId.${commentId}`,
        {id : commentId, text : commentText}
    );

    const updatedWithCommentsList = dotProp.set(
        updatedWithCommentsTable,
        `comments.allIds`,
        allIds => allIds.concat(commentId);
    );

    return updatedWithCommentsList;
}

const featureReducers = createReducer({}, {
    ADD_COMMENT : addComment,
});

const rootReducer = reduceReducers(
    combinedReducer,
    featureReducers
);
이 접근 방식을 사용하면 "ADD_COMMENTS" 경우에 어떤 일이 일어나고 있는지 매우 명확하게 알 수 있지만
중첩 업데이트 논리와 상태 트리 모양에 대한 특정 지식이 필요합니다.
리듀서 로직을 합성하는 방법에 따라 이것이 바람직할 수도 있고 그렇지 않을 수도 있습니다.

 

Redux-ORM

Redux-ORM 라이브러리는 Redux 저장소에서 정규화된 데이터를 관리하기 위한 매우 유용한 추상화 계층을 제공합니다.
Model 클래스를 선언하고 그들 사이의 관계를 정의할 수 있습니다.
그런 다음 데이터 타입에 대한 빈 "테이블"을 생성하고, 데이터를 조회하기 위한 특수 선택 도구로 작동하고, 해당 데이터에 대해 변경할 수 없는 업데이트를 수행할 수 있습니다.
Redux-ORM을 사용하여 업데이트를 수행할 수 있는 몇 가지 방법이 있습니다.
먼저 Redux-ORM 문서에서는 각 Model 하위 클래스에서 리듀서 함수를 정의한 다음 자동 생성된 combinedReducer 함수를 스토어에 포함할 것을 제안합니다.
// models.js
import { Model, fk, attr, ORM } from 'redux-orm'

export class Post extends Model {
  static get fields() {
    return {
      id: attr(),
      name: attr()
    }
  }

  static reducer(action, Post, session) {
    switch (action.type) {
      case 'CREATE_POST': {
        Post.create(action.payload)
        break
      }
    }
  }
}
Post.modelName = 'Post'

export class Comment extends Model {
  static get fields() {
    return {
      id: attr(),
      text: attr(),
      // Define a foreign key relation - one Post can have many Comments
      postId: fk({
        to: 'Post', // must be the same as Post.modelName
        as: 'post', // name for accessor (comment.post)
        relatedName: 'comments' // name for backward accessor (post.comments)
      })
    }
  }

  static reducer(action, Comment, session) {
    switch (action.type) {
      case 'ADD_COMMENT': {
        Comment.create(action.payload)
        break
      }
    }
  }
}
Comment.modelName = 'Comment'

// Create an ORM instance and hook up the Post and Comment models
export const orm = new ORM()
orm.register(Post, Comment)

// main.js
import { createStore, combineReducers } from 'redux'
import { createReducer } from 'redux-orm'
import { orm } from './models'

const rootReducer = combineReducers({
  // Insert the auto-generated Redux-ORM reducer.  This will
  // initialize our model "tables", and hook up the reducer
  // logic we defined on each Model subclass
  entities: createReducer(orm)
})

// Dispatch an action to create a Post instance
store.dispatch({
  type: 'CREATE_POST',
  payload: {
    id: 1,
    name: 'Test Post Please Ignore'
  }
})

// Dispatch an action to create a Comment instance as a child of that Post
store.dispatch({
  type: 'ADD_COMMENT',
  payload: {
    id: 123,
    text: 'This is a comment',
    postId: 1
  }
})
Redux-ORM 라이브러리는 모델 간의 관계를 유지합니다.
업데이트는 기본적으로 변경 불가능하게 적용되어 업데이트 프로세스를 단순화합니다.
이에 대한 또 다른 변형은 단일 케이스 리듀서 내에서 Redux-ORM을 추상화 계층으로 사용하는 것입니다.
import { orm } from './models'

// Assume this case reducer is being used in our "entities" slice reducer,
// and we do not have reducers defined on our Redux-ORM Model subclasses
function addComment(entitiesState, action) {
  // Start an immutable session
  const session = orm.session(entitiesState)

  session.Comment.create(action.payload)

  // The internal state reference has now changed
  return session.state
}
세션 인터페이스를 사용하면 이제 관계 접근자를 사용하여 참조된 모델에 직접 액세스할 수 있습니다.
const session = orm.session(store.getState().entities)
const comment = session.Comment.first() // Comment instance
const { post } = comment // Post instance
post.comments.filter(c => c.text === 'This is a comment').count() // 1
Redux-ORM은
  • 데이터 타입 간의 관계를 정의하고,
  • 우리 상태에서 "테이블"을 생성하고,
  • 관계형 데이터를 검색 및 비정규화하고,
  • 관계형 데이터에 변경 불가능한 업데이트를 적용하기 위한 매우 유용한 추상화 세트를 제공합니다.

 

 

Redux toolkit

공식 문서에는 없지만, Redux toolkit을 사용하면, 위의 기능을 createEntityAdapter가 대체합니다. 

리덕스 툴킷을 사용한다면 위 내용은 크게 의미가 없습니다.

반응형