본문으로 건너뛰기
비공식 베타 번역

이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →

정규화된 데이터 관리

정규화된 상태 구조에서 언급했듯이, Normalizr 라이브러리는 중첩된 응답 데이터를 스토어 통합에 적합한 정규화된 형태로 변환하는 데 자주 사용됩니다. 하지만 애플리케이션 다른 부분에서 사용 중인 정규화된 데이터에 대한 추가 업데이트를 실행하는 문제는 해결하지 못합니다. 여러분의 선호도에 따라 다양한 접근 방식을 사용할 수 있습니다. 여기서는 게시물(Post)에 대한 댓글(Comment) 변경 사항을 처리하는 예제를 사용하겠습니다.

표준 접근 방식

단순 병합

한 가지 접근 방식은 액션의 내용을 기존 상태에 병합하는 것입니다. 이 경우 부분 항목이 포함된 액션으로 저장된 항목을 업데이트할 수 있도록 얕은 복사가 아닌 깊은 재귀 병합을 사용할 수 있습니다. 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
}
}
}

이 방식은 리듀서 측에서 가장 적은 작업을 요구하지만, 액션이 디스패치되기 전에 액션 생성자가 데이터를 올바른 형태로 구성하기 위해 상당한 작업을 수행해야 할 수 있습니다. 또한 항목 삭제를 처리하지 못합니다.

슬라이스 리듀서 구성

중첩된 슬라이스 리듀서 트리가 있는 경우, 각 슬라이스 리듀서는 이 액션에 적절히 응답하는 방법을 알아야 합니다. 액션에 관련된 모든 데이터를 포함해야 합니다. 올바른 Post 객체에 댓글 ID를 업데이트하고, 해당 ID를 키로 사용해 새 Comment 객체를 생성하며, 모든 댓글 ID 목록에 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
})

이 예제는 서로 다른 슬라이스 리듀서와 케이스 리듀서가 어떻게 조화를 이루는지 보여주기 때문에 조금 깁니다. 여기서 위임이 어떻게 이루어지는지 주목하세요. postsById 슬라이스 리듀서는 이 케이스에 대한 작업을 addComment에 위임하며, 이 함수는 새 댓글의 ID를 올바른 Post 항목에 삽입합니다. 한편 commentsByIdallComments 슬라이스 리듀서는 각각 자체 케이스 리듀서를 가지고 있어 댓글 조회 테이블과 모든 댓글 ID 목록을 적절히 업데이트합니다.

다른 접근법

작업 기반 업데이트

리듀서는 함수일 뿐이므로 이 로직을 분할하는 방법은 무한히 많습니다. 슬라이스 리듀서를 사용하는 것이 가장 일반적이지만, 작업 중심 구조로 동작을 구성하는 것도 가능합니다. 이 경우 종종 더 많은 중첩 업데이트가 수반되므로 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 스토어에서 정규화된 데이터를 관리하기 위한 매우 유용한 추상화 계층을 제공합니다. 모델 클래스를 선언하고 그들 간의 관계를 정의할 수 있습니다. 그런 다음 데이터 유형에 대한 빈 "테이블"을 생성하고, 데이터 조회를 위한 특화된 선택자 도구로 작동하며, 해당 데이터에 대한 불변 업데이트를 수행할 수 있습니다.

Redux-ORM으로 업데이트를 수행하는 몇 가지 방법이 있습니다. 첫째, Redux-ORM 문서는 각 모델 서브클래스에 리듀서 함수를 정의한 다음 자동 생성된 결합 리듀서 함수를 스토어에 포함할 것을 제안합니다:

// 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은 데이터 타입 간의 관계 정의, 상태 내 '테이블' 생성, 관계형 데이터 검색 및 비정규화, 관계형 데이터에 대한 불변 업데이트 적용을 위한 매우 유용한 추상화 도구 세트를 제공합니다.