우아한 Tech React&TypeScript #5

sangha__ju·2020년 9월 19일
0

JS로 구현 한 Reudx Review

지난 번 작성 한 Redux 코드를 인용하여 다음과 같이 store / reducer를 생성한다.

import { createStore } from "./redux";

function reducer(state = { counter: 0 }, action) {
    switch (action.type) {
        case "INC":
            return {
                ...state,
                counter: state.counter + 1,
            };
        default:
            return { ...state };
    }
}

const store = createStore(reducer);

상태의 변경 사항을 알기 위해 subcribe 메서드를 이용하여 변경 사항을 확인한다.

store.subscribe(() => {
    console.log(store.getState());
});

subcribe 메서드의 인자인 콜백 함수는 store 의 상태를 반환하는 getState 메서드를 콘솔로 출력한다.

store.dispatch({ type: "INC" }); // { counter: 1 }
store.dispatch({ type: "INC" }); // { counter: 2 }

위 로직들이 따로 작동하는 것 처럼 보이지만 동기적으로 진행된다.
모든 로직이 동기적으로 작동하기 때문에 reducer 는 반드시 순수 함수여야 한다.

멱등성: 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질

Redux 를 사용하는 로직에서 실행 할 때마다 결과가 다를 수 있는 경우가 존재한다.
순수하지 않은 로직의 예시로는 비동기 작업이 있으며 API 통신이 대표적이다.

커링

const add1 = function (a, b) {
    return a + b;
};

const add2 = function (a) {
    return function (b) {
        return a + b;
    };
};

add1 함수는 1차 함수, add2 함수는 함수를 반환하는 2차 함수로 작성 되어있다.
add2 함수는 아래와 같이 함수가 실행되는 시점에 영향을 줄 수 있다.

const addTen = add2(10);
console.log(addTen(20)); // 30
console.log(addTen(120)); // 130

add1 함수의 경우 add(10, 20) 과 같이 호출하게 되면 바로 실행된다.
add2 함수는 위와 같이 한번 더 함수를 호출해야 함수가 지연 호출 되게 된다.

Redux의 비동기와 미들웨어

type이 FETCH_USER 비동기 로직을 처리하는 액션이 있다고 가정 해 본다.

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

setTimeout을 사용하여 비동기와 동일하게 동작하는 api 함수를 생성한다.

function api(url, cb) {
    setTimeout(() => {
        cb({ type: "응답", data: [] });
    }, 2000);
}

reducer 에 FETCH_USER 를 처리하는 case 문을 추가한다.

function reducer(state = { counter: 0 }, action) {
    switch (action.type) {
        case "INC":
            return {
                ...state,
                counter: state.counter + 1,
            };
        case "FETCH_USER":
            api("/api/users/1", (users) => {
                return { ...state, ...users };
            });
        default:
            return { ...state };
    }
}

하지만 Redux 에서는 모든 로직을 동기적으로 처리하기 때문에 값을 기다리지 않는다.
따라서 비동기 작업을 처리하고 싶을 때는 미들웨어를 사용해야 한다.

데이터가 흘러가는 과정 중간에 거쳐가기 때문에 미들웨어라고 불린다.
미들웨어는 연결된 순서에 따라 흐르기 때문에 항상 같은 순서대로 처리된다.

const middlewareOne = (store) => (dispatch) => (action) => {
    dispatch(action);
};

function middlewareTwo(store) {
    return function (dispatch) {
        return function (action) {
            dispatch(action);
        };
    };
}

function middlewareThree(store, dispatch, action) {
    dispatch(action);
}

middlewareOne / middlewareTwo 함수는 3개의 인자를 3개의 함수로 나눈 커링을 이용한다.
커링을 이용한 middlewareOne 과 middlewareTwo 함수는 다음과 같이 실행 가능하다.

middlewareOne(store)(store.dispatch)({ type: "INC" });
middlewareTwo(store)(store.dispatch)({ type: "INC" });

하지만 매개변수가 3개인 middlewareThree 는 인자 3개를 한번에 전달한다.

middlewareThree(store, store.dispatch, { type: "INC" });

INC 액션이 동기적으로 처리되며 store 의 counter 가 1씩 증가 된다.
그럼 왜 커링을 사용하여 미들웨어를 작성하는지 생각 해 보아야 한다.
reducer 에서 로깅을 하려고 할 때 reducer 를 직접 접근 할 수 없을 때 다음과 같이 로깅 할 수 있을 것이다.

console.log('action -> { type: "INC" }');
store.dispatch({ type: "INC" });

console.log('action -> { type: "INC" }');
store.dispatch({ type: "INC" });

액션이 dispatch 되는것은 로깅을 할 수 있지만 모든 액션마다 직접 로깅을 해야한다는 단점이 있다.
이 단점을 해결하지 위해 함수로 액션을 감싸 dispatch 를 할 수 있다.

function dispatchAndLog(store, action) {
    console.log("dispatching", action);
    store.dispatch(action);
    console.log("next state", store.getState());
}

dispatchAndLog(store, { type: "INC" });
dispatchAndLog(store, { type: "INC" });

각 액션에 로깅을 직접하지 않아도 간단하게 실행 된 액션을 로깅 할 수 있다.

Redux 에 미들웨어 추가하기

Redux 에 미들웨어를 접목 시켜 보자.

const logger = (store) => (next) => (action) => {
    console.log("logger:", action.type);
    next(action);
};

const monitor = (store) => (next) => (action) => {
    setTimeout(() => {
        console.log("monitor:", action.type);
        next(action);
    }, 2000);
};

logger 미들웨어는 동기적으로 실행되지만 monitor 미들웨어는 비동기적으로 실행된다.
작성 한 두개의 미들웨어를 적용하기 위한 applyMiddleware 함수를 만든다.

export function applyMiddleware(store, middlewares = []) {
    middlewares = middlewares.slice();
    middlewares.reverse();

    let dispatch = store.dispatch;
    middlewares.forEach(
        (middleware) => (dispatch = middleware(store)(dispatch))
    );

    return Object.assign({}, store, { dispatch });
}

middlewares 매개 변수를 받으며 기본값은 비어있는 배열이다.
또한 미들웨어를 적용하기 위해서는 middlewares 인자로 받은 미들웨어 배열을 뒤집어야 한다.

middlewares.reverse();

배열을 뒤집는 이유는 미들웨어가 최종적으로 실행하는 dispatch 는 store 의 dispatch 함수여야 하기 때문이다.
middleware(store)(dispatch) 형태로 호출되며 반환값은 계속해서 왼쪽 미들웨어에 전달된다.

배열을 뒤집음으로써 뒤집기 전의 가장 뒤인 오른쪽에서 왼쪽으로 dispatch 가 된다.
결국 최종 실행되는 dispatch 함수는 기존 store 의 dispatch 함수가 되게 된다.

const store = applyMiddleware(createStore(reducer), [logger, monitor]);
profile
NexCloud(Nexclipper) FrontEnd Developer / Personal Learning Storage

0개의 댓글