티스토리 뷰

Javascript/React

#6. 리덕스로 상태 관리하기

rivercity310 2021. 12. 22. 11:15



리덕스는 자바스크립트를 위한 상태 관리 프레임워크입니다.
흔히들 리액트를 사용할 때 리덕스도 같이 사용합니다.


리액트에서 데이터 관리를 위한 다양한 방법들이 있지만 굳이 리덕스를 사용하는 이유는 무엇일까요?
리덕스를 사용했을 때 이점은 다음과 같습니다.

  • 컴포넌트 코드로부터 상태 관리 코드를 분리
  • 같은 상탯값을 다수의 컴포넌트에서 필요로 할 때 좋음
  • 페이지가 전환되어도 데이터는 살아 있어야 할 때 좋음



리덕스 사용 시 따라야 할 세가지 원칙이 있습니다.

  • 전체 상탯값은 하나의 객체에 저장한다.
  • 상탯값은 불변 객체로 관리한다.
  • 상탯값은 순수 함수에 의해서만 변경되어야 함.



기본적인 리듀서의 구조는 아래와 같습니다.
스토어는 상태를 가지고 있으고, 컴포넌트는 디스패치 함수를 통해 액션을 리듀서로 전달합니다.
그리고 스토어의 상태는 리듀서에 의해서만 변경이 가능하고, 컴포넌트는 이러한 상태를 구독하게 되는 것입니다.
좀 더 나아가면 미들웨어라는 내용이 추가되는데, 미들웨어는 리듀서와 액션사이에서 어떤 역할을 수행하게 되고 비동기 작업등을 처리하는 역할을 맡게 됩니다.

출처; https://stackoverflow.com/questions/51610872/storing-to-redux-on-page-load/51611975




 

1. 액션



액션은 type 속성값을 가진 자바스크립트 객체입니다.
액션 객체를 dispatch 메서드에 넣어서 호출하면 리덕스는 상탯값을 변경하기 위해 리듀서로 액션을 전달합니다.

각 액션은 고유한 type 속성값을 사용하는데, 중복 가능성이 있으므로 접두사를 붙이는 방법이 많이 사용됩니다.
또한 dispatch 메서드를 호출할 때 직접 액션 객체를 입력하는 방법은 지양하고,
액션 생성자 함수를 따로 만들어 호출하는 방식이 좋습니다.
또한 액션 타입은 외부 리듀서 함수에서도 사용하므로 따로 상수 변수로 만들어 둡니다.

// 액션 타입은 상수 변수로 만드는 것이 좋다.
export const ADD = "todo/ADD";
export const REMOVE = "todo/REMOVE";
export const REMOVE_ALL = "todo/REMOVE_ALL";

// 액션 생성자 함수 
export function addTodo({ title, priority }) {
    return { type: ADD, title, priority };
}

export function removeTodo({ id }) {
    return { type: REMOVE, id };
}

export function removeAllTodo() {
    return { type: REMOVE_ALL }; 
} 

// dispatch 메서드를 호출할 때 액션 생성자 함수를 이용 
store.dispatch(addTodo({ title: "영화 보기", priority: "high" }));
store.dispatch(removeTodo({ id: 123 })); store.dispatch(removeAllTodo());

 


 

2. 미들웨어



미들웨어는 리듀서가 액션을 처리하기 전에 실행되는 함수입니다.
그 쓰임새로는 디버깅 목적으로 상탯값 변경 시 로그를 출력하거나, 리듀서에서 발생한 예외를 처리하는 등의 목적이 있습니다. 미들웨어의 기본 구조는 다음과 같습니다.

const Middleware = store => next => action => next(action);



미들웨어는 스토어와 액션 객체를 기반으로 필요한 작업을 수행합니다.
next 함수를 호출하면 다음 미들웨어 함수가 호출되고 최종적으로 리듀서 함수가 호출됩니다.

다음 코드에서 middleware1 미들웨어에서 호출한 next 함수는 middleware2 미들웨어 함수를 실행합니다.
middleware2 미들웨어에서 호출한 next 함수는 스토어의 dispatch 메서드를 호출하고, 스토어의 dispatch 메서드가 리듀서를 호출합니다.

import { createStore, applyMiddleware } from "redux";


const middleware1 = store => next => action => {
    console.log("middleware1 start");
    const result = next(action);
    console.log("middleware1 end");
    return result;
}

const middleware2 = store => next => action => {
    console.log("middleware2 start");
    const result = next(action);
    console.log("middleware2 end");
    return result;
}

const myReducer = (state, action) => {
    console.log("myReducer");
    return state;
}

const store = createStore(myReducer, applyMiddleware(middleware1, middleware2));

store.dispatch({ type: "someAction" });



// middleware1 start
// middleware2 start
// myReducer
// middleware2 end
// middleware1 end



액션 객체를 store의 dispatch 메서드에 넣어 호출하면 상탯값을 변경하기 위해 reducer 함수를 호출하게 되는데,
그 전에 미들웨어 함수가 정의되어 있다면 미들웨어 함수가 호출되므로 액션이 발생할 때마다 이전 상탯값과 이후 상탯값을 활용한 디버깅 목적의 로그를 출력하거나 로컬 스토리지에 값을 저장하는 등 다양한 처리를 할 수 있습니다.

// 로그를 출력해주는 미들웨어
const printLog = store => next => action => {
    console.log(`prev = ${store.getState()}`);
    const result = next(action);
    console.log(`next = ${store.getState()}`);
    return result;
}

// ================================================================= 

// 에러 정보를 서버로 전송해주는 미들웨어 
const reportCrash = store => next => action => {
    try {
        return next(action);
    } catch (error) {
        // 서버로 예외 정보 전송 
    }
}

// ================================================================= 

// 사용자가 원하는 경우 액션 처리를 일정 시간 연기 
const delayAction = store => next => action => {
    const delay = action.meta && action.meta.delay;
    if (!delay) {
        return next(action);
    }

    const timeoutID = setTimeout(() => next(action), delay);
    return function cancle() {
        clearTimeout(timeoutID);
    }
}

// =================================================================

// 로컬 스토리지에 값을 저장하는 미들웨어 
const saveToLocalStorage = store => next => action => {
    if (action.type === "SET_NAME") {
        localStorage.setItem("name", action.name);
    }
    return next(action);
}

const myReducer = (state, action) => {
    return state;
}

const store = createStore(myReducer, applyMiddleware(printLog, reportCrash, delayAction, saveToLocalStorage));

// 액션 객체에 delay 정보가 포함되어 있다면 정해진 시간만큼 연기 
const cancle = store.dispatch({ type: "someAction", meta: { delay: 1000 }, })

// 반환된 함수를 호출하면 next 함수 호출을 막음 
cancle();

 


 

3. 리듀서



리듀서는 액션이 발생했을 때 새로운 상탯값을 만드는 함수입니다.
구조는 다음과 같습니다.

(state, action) => nextState



리덕스는 스토어를 생성할 때 상탯값이 없는 상태로 리듀서를 호출하므로 매개변수의 기본값을 사용하여
초기 상탯값을 정의힙낟. 상탯값은 불변 객체로 관리해야 하므로 수정할 때마다 전개 연산자를 사용하여 새로운 객체를 생성해주어야 합니다.

function reducer(state = INITIAL_STATE, action) {
    switch (action.type) {
        case REMOVE_ALL:
            return {
                ...state,
                todos: [],
            };
        case REMOVE:
            return {
                ...state,
                todos: state.todos.filter(todo => todo.id !== action.id),
            };
        default:
            return state;
    }
}

const INITIAL_STATE = { todos: [] };




그런데 만약 수정하려는 상탯값이 깊은 곳에 있다면 전개 연산자를 사용하더라도 수정이 쉽지 않습니다.
immer 패키지 produce 함수의 첫번째 매개변수는 변경하고자 하는 객체, 두번째 매개변수는 첫번째 매개변수에 입력된 객체를 수정하는 함수입니다. 아래 newPerson에서 draft.age를 수정해도 person 객체의 값은 변경되지 않고 값이 수정된 새로운 객체가 newPerson에 할당됩니다. 따라서 immer를 사용한 리듀서 함수에서도 배열의 push와 같은 메서드를 사용해도 기존 상탯값이 직접 수정되지 않고 새로운 객체가 생성되어 반환됩니다.

function reducer(state = INITIAL_STATE, action) {
    switch (action.type) {
        case ADD:
            return {
                ...state,
                todos: [...state.todos,
                { id: getNewID(), title: action.title, priority: action.priority },
                ]
            }
    }
}

// ============================================================================= 

// 이머 패키지 사용 
import produce from "immer";
const person = { name: "mike", age: 22 };
const newPerson = produce(person, draft => {
    draft.age = 32;
})

// 위 리듀서 함수를 이머를 사용해 리팩터링한 코드 
function reducer(state = INITIAL_STATE, action) {
    return produce(state, draft => {
        switch (action.type) {
            case ADD:
                draft.todos.push(action.todo);
                break;
            case REMOVE_ALL:
                draft.todos = [];
                break;
            case REMOVE:
                draft.todos = draft.todos.filter(todo => todo.id !== action.id);
                break;
            default:
                break;
        }
    })
}




createReducer 함수로 리듀서 작성

지금까지 리듀서 함수를 작성할 때 switch 문을 사용했지만, createReducer 함수를 이용하면 더 간결하게 작성할 수 있습니다. createReducer 함수는 리덕스에서 제공되는 함수는 아니지만 리덕스 생태계에서 많이 쓰입니다.

const reducer = createReducer(INITIAL_STATE, {
    [ADD]: (state, action) => state.todos.push(action.todo),
    [REMOVE_ALL]: state => (state.todos = []),
    [REMOVE]: (state, action) => (state.todos = state.todos.filter(todo => todo.id !== action.id)),
})


createReducer 함수의 첫번째 인자로 초기 상탯값을, 두번째 인자로 액션 처리 함수를 담고있는 객체를 전달했습니다. switch 문을 사용한 것보다 간결해 보입니다.
또한 push와 같은 배열 메서드를 사용하는 것을 보니 createReducer 함수도 immer 패키지를 사용하는 것 같습니다.
createReducer 함수는 다음과 같이 정의되어 있습니다.

import produce from "immer";

function createReducer(initialState, handlerMap) {
    return function (state = initialState, action) {
        return produce(state, draft => {
            const handler = handlerMap[action.type];
            if (handler) {
                handler(draft, action);
            }
        })
    }
}

 


 

4. 스토어


스토어는 리덕스의 상탯값을 가지는 객체입니다.
액션의 발생이 스토어의 dispatch 메서드로부터 시작됩니다.
스토어는 액션이 발생하면 미들웨어 함수를 실행하고 리듀서를 실행해서 상탯값을 새로운 값으로 변경합니다.

또한 외부에 상탯값 변경 여부를 알리기 위해 스토어에 이벤트 처리 함수를 등록할 수 있습니다.
다음은 스토어의 subscribe 메서드를 사용해서 상탯값 변경 여부를 검사하는 코드입니다.

function createReducer(initialState, handlerMap) {
    return function (state = initialState, action) {
        return produce(state, draft => {
            const handler = handlerMap[action.type];
            if (handler) {
                handler(draft, action);
            }
        })
    }
}

const INITIAL_STATE = { value: 0 };
const reducer = createReducer(INITIAL_STATE, {
    INCREMENT: state => (state.value += 1)
});

const store = createStore(reducer);

let prevState; store.subscribe(() => {
    const state = store.getState();
    if (state === prevState) {
        console.log("상탯값 같음");
    } else {
        console.log("상탯값 변경됨");
    }
    prevState = state;
})

store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "OTHER_ACTION" });

// 상탯값 변경됨
// 상탯값 같음

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함