ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Redux | Redux 개요 및 주요 개념
    JavaScript/JavaScript 2020. 5. 14. 17:11

    Redux

    리덕스는 자바스크립트를 위한 상태 관리 프레임워크


    리덕스 사용 이점

    • 컴포넌트 코드로부터 상태 관리 코드 분리 가능
    • 서버 렌더링 시 데이터 전달 간편
    • 로컬 스토리지에 데이터를 저장하고 불러오는 코드를 쉽게 작성 가능
    • 같은 상탯값을 다수의 컴포넌트에서 필요로 할 때 좋음
    • 부모 컴포넌트에서 깊은 곳에 있는 자식 컴포넌트에 상탯값을 전달할 때 좋음
    • 알림창과 같은 전역 컴포넌트의 상탯값을 관리할 때 좋음
    • 페이지가 전환되어도 데이터는 살아 있어야 할 때 좋음

    리덕스 3 원칙

    리덕스 공식 문서에서 제시하는 리덕스 사용 세 가지 원칙

    1. 전체 상탯값을 하나의 객체에 저장
    2. 상탯값은 불변 객체
    3. 상탯값은 순수 함수에 의해서만 변경되어야 함

    ① 전체 상탯값을 하나의 객체에 저장

    • 전체 상탯값이 하나의 객체로 관리되므로 활용도가 높아짐
    • 리덕스를 사용하면 하나의 객체를 직렬화해서 서버와 클라이언트가 프로그램의 전체 상태를 주고 받을 수 있음
    • 최근의 상탯값을 버리지 않고 저장해 두면 실행 취소(undo)와 다시 실행(redo) 기능을 쉽게 구현 가능

    ※ 특별히 로직이 복잡하지 않은 페이지에서는 컴포넌트의 상탯값을 활용하는 게 생산성을 더 높일 수 있으며,

    시간 여행과 같은 기능을 구현하는 게 아니라면 필요한 곳에서만 리덕스를 사용해도 됨


    ② 상탯값을 불변 객체로 관리

    상탯값은 오직 액션 객체에 의해서만 변경되어야 함

    // 액션 객체
    const incrementAction = {
        type: "INCREMENT",
        amount: 123,
    }
    // 상탯값 저장소로 액션 객체 전달
    store.dispatch(incrementAction);
    • 액션 객체에는 type 속성값이 반드시 존재해야 함
      type 속성값으로 액션 객체 구분
    • type 을 제외한 나머지는 상탯값을 수정하기 위해 사용되는 정보
    • 액션 객체와 함께 dispatch 메서드를 호출하면 상탯값이 변경됨
    • 상탯값 수정이라는 목적만 놓고 보면 불변 객체를 사용하는 것이 불리해 보이지만,
      이전 상탯값과 이후 상탯값을 비교해서 변경 여부를 파악할 때는 불변 객체가 훨씬 유리함
    • 상탯값 변경을 빠르게 확인할 수 있으면 메모이제이션과 같은 기능을 활용하기 좋고,
      리액트의 렌더링 성능을 올리는 데도 유리함

    ③ 오직 순수 함수에 의해서만 상탯값을 변경해야 함: 리듀서

    리덕스에서 상탯값을 변경하는 함수를 리듀서(reducer)라고 함

    Reducer 리듀서

    // 리듀서 기본 구조
    (state, action) => nextState
    • 리듀서는 이전 상탯값과 액션 객체를 입력 받아서 새로운 상탯값을 만드는 순수 함수
    • 순수 함수는 부수 효과(side effect)를 발생시키지 않아야 하며,
      같은 인수에 대해 항상 같은 값을 반환해야 함
      ex) 랜덤/시간 함수를 이용하면 순수함수가 아님
    • 부수 효과란 전역 변수의 값을 수정하거나 API 요청을 보내는 등 함수 외부의 상태를 변경시키는 것을 말함
    • 순수 함수는 테스트 코드를 작성하기 쉬움

    리덕스 주요 개념

    리덕스 흐름


    액션 Action

    액션은 type 속성을 갖는 자바스크립트 객체

    • 액션 객체를 dispatch 메서드에 넣어서 호출하면 리덕스는 상탯값을 변경하기 위해 위 그림의 과정을 수행
    • 액션 객체에는 type 속성 외에도 원하는 속성을 얼마든지 넣을 수 있음
    // 액션 생성자 함수
    function addTodo({ title, priority }) {
        return { type: 'todo/ADD', title, priority };
    }
    function removeTodo({ id }) {
        return { type: 'todo/REMOVE', id };
    }
    
    // dispatch 메서드 호출
    store.dispatch(addTodo({ title: '영화 보기', priority: 'high'}));
    store.dispatch(removeTodo({ id: 123 }));
    • 각 액션은 고유한 type 속성값을 사용해야 함
    • type 이름 충돌을 피하기 위해 'todo/' 와 같은 접두사를 붙이는 방법이 많이 사용됨
    • 액션 객체를 dispatch 메서드에 직접 전달하는 방법보다는 생성자 함수를 사용하는 방법이 좋음
      → 액션 객체의 구조를 변경할 때 생성자 함수만 수정하면 됨
    • type 속성값은 리듀서에서 액션 객체를 구분할 때도 사용되기 때문에 상수 변수로 만드는 게 좋음
    // 액션 타입을 상수 변수로 관리
    export const ADD = 'todo/ADD';
    export const REMOVE = 'todo/REMOVE';
    
    // 생성자 함수도 같은 파일에서 관리하며 외부로 노출
    export function addTodo({ title, priority }) {
        return { type: ADD, title, priority };
    }
    export function removeTodo({ id }) {
        return { type: REMOVE, id };
    }
    • 액션 생성자 함수에서는 부수 효과를 발생시켜도 괜찮음
    • 예를 들어 addTodo 함수에서 새로운 할일을 서버에 저장하기 위해 API 호출을 할 수 있음

    미들웨어 Middleware

    미들웨어는 리듀서가 액션을 처리하기 전에 실행되는 함수

    • 디버깅 목적으로 상탯값 변경 시 로그를 출력하거나, 리듀서에서 발생한 예외를 서버로 전송하는 등의 목적으로 활용 가능
    • 미들웨어를 설정하지 않았다면 액션은 바로 리듀서로 보내짐

    미들웨어 기본 구조

    const myMiddleware = store => next => action => next(action);
    
    // 화살표 함수를 사용하지 않은 코드
    const myMiddleware = function(store) {
        return function(next) {
            return function (action) {
                return next(action);
            }
        }
    }
    • 함수 세 개가 중첩된 구조
    • 미들웨어는 스토어와 액션 객체를 기반으로 필요한 작업 수행
    • next 함수를 호출하면 다른 미들웨어 함수가 호출되면서 최종적으로 리듀서 함수 호출

    미들웨어 설정하기

    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
    */
    • 같은 기능을 하는 간단한 미들웨어 정의
    • applyMiddleware 함수로 미들웨어가 입력된 스토어 생성
    • middleware1 에서 호출한 next 함수는 middleware2 함수를 실행
    • middleware2 에서 호출한 next 함수는 스토어가 원래 갖고 있던 dispatch 메서드 호출
    • 최종적으로 스토어의 dispatch 메서드는 리듀서를 호출
    • 각 미들웨어에서 리듀서 호출 전후에 필요한 작업 정의 가능

    미들웨어 활용 예

    로그 출력 미들웨어

    next 함수를 호출하면 리듀서가 호출되기 때문에 next 함수 전후로 로그 출력

    const printlog = store => next => action => {
        console.log(`prev state = ${store.getState()}`);
        const result = next(action);
        console.log(`next state = ${store.getState()}`);
        return result;
    }

    로컬 스토리지에 값을 저장하는 미들웨어

    'SET_NAME' 액션이 발생할 때마다 로컬 스토리지에 값을 저장

    const saveToLocalStorage = store => next => action => {
        if (action.type === 'SET_NAME'){
            localStorage.setItem('name', action.name);
        }
        return next(action);
    }

    리듀서 Reducer

    액션이 발생했을 때 새로운 상탯값을 만드는 함수


    리듀서 기본 구조

    (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: [] };
    • 리덕스는 스토어 생성 시 상탯값이 없는 상태로 리듀서를 호출함으로 매개변수의 기본값을 사용해서 초기 상탯값 정의
    • 각 액션 타입별로 case 문을 만들어서 처리
    • 상탯값은 불변 객체로 관리해야함으로 수정할 때마다 새로운 객체 생성,
      ...(전개 연산자)를 사용하면 상탯값을 불변 객체로 관리 가능
    • default: 처리할 액션이 없다면 상탯값을 변경하지 않음

    immer 패키지

    불변 객체 관리를 목적으로 하는 패키지
    깊은 곳에 있는 값을 수정할 때는 전개 연산자를 사용해도 가독성이 떨어지기 때문에 사용


    immer를 이용한 리듀서 작성

    import produce from 'immer';
    
    function reducer(state = INITIAL_STATE, action) {
        return produce(state, draft => {
            switch (action.type) {
                case ADD:
                    draft.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;
            }
        })
    }
    
    const INITIAL_STATE = { todos: [] };
    • switch문 전체를 produce 함수로 감쌈
    • produce 함수의 첫 번째 매개변수는 변경하고자 하는 객체
    • 두 번째 매개변수는 첫 번째 매개변수로 입력된 객체를 수정하는 함수
      draft 매개변수를 state 객체라고 생각하면 됨
    • immer를 사용했기 때문에 push와 같이 배열을 직접 변경하는 메서드도 사용 가능
    • draft.todos 를 수정해도 state 객체의 값은 변경되지 않음
      draft 객체를 수정하면 produce 함수가 새로운 객체를 반환해 줌

    리듀서 작성 시 주의할 점

    1. 데이터 참조
      데이터를 참조할 때 객체의 레퍼런스를 참조하는 경우, 해당 객체가 수정되면 더 이상 같은 데이터를 참조할 수 없게 될 수 있기 때문에 객체의 레퍼런스보다는 ID 값으로 데이터를 참조하는 것이 좋음
      → ID 값으로 참조하면 객체의 데이터가 변경되어도 별문제 없이 데이터를 가리킬 수 있음
    2. 순수 함수
      리듀서는 순수 함수로 작성해야 함
      랜덤 함수를 이용해서 다음 상탯값을 만들거나, API를 호출해서는 안 됨
      → API 호출은 액션 생성자 함수나 미들웨어에서 하면 됨

    createReducer 함수로 리듀서 작성

    switch 문보다 간결하게 리듀서 함수를 작성할 수 있는 함수
    리덕스에서 제공하는 함수는 아니지만 리덕스 생태계에서 많이 쓰임


    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);
                }
            })
        }
    }
    • createReducer 함수는 리듀서를 반환
    • 리듀서 함수 전체를 immer의 produce 함수로 감쌈
    • 등록된 액션 처리 함수가 있다면 실행

    createReducer 함수로 작성한 리듀서 함수

    const reducer = createReducer(INITAIL_STATE, {
        [ADD]: (state, action) => state.todos.push(action.todo),
        [REMOVE_ALL]: state => (state.todos = []),
        [ROMOVE]: (state, action) => 
            (state.todos = state.todos.filter(todo => todo.id !== action.id)),
    })
    • createReducer 함수의 첫 번째 인자로 초기 상탯값 입력
    • createReducer 함수의 두 번째 인자는 액션 처리 함수를 담고 있는 객체
    • switch문으로 작성한 것보다 코드가 간결해짐

    스토어

    스토어는 리덕스의 상탯값을 갖는 객체
    액션의 발생은 스토어의 dispatch 메서드로 시작됨

    • 리덕스의 첫 번째 원칙은 전체 상탯값을 하나의 스토어에 저장하는 것
    • 기술적으로는 여러 개의 스토어를 만들어서 사용해도 문제가 되지 않음
      → 그러나 특별한 이유가 없다면 하나만 만드는 것이 좋음
    • 데이터의 종류에 따라 구분하기 위한 용도라면 combineReducer 함수를 이용

    subscribe 메서드 사용 예제

    외부에서도 상탯값 변경 여부를 알기 위해 store의 subscribe 메서드를 이용해서 스토어에 이벤트 처리 함수 등록

    const INITIAL_STATE = { value: 0 };
    const reducer = createReducer(INITAIL_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' });
    store.dispatch({ type: 'INCREMENT' });
    • subscribe 메서드를 이용해서 이베트 처리 함수 등록
    • 스토어에 등록된 함수는 액션이 처리될 때마다 호출
    • 상탯값이 불변 객체이므로 간단한 비교로 변경 여부 확인 가능
    • 'INCREMENT' 액션이 발생하면 '상탯값 변경됨' 로그 출력
    • 등록되지 않은 액션 발생 시 '상탯값 같음' 출력

     

    리액트에서 데이터를 효율적으로 관리하기 위해 리덕스에 대해 공부하고 있습니다.

     

    참고도서: [ 실전 리액트 프로그래밍 / 저자_ 이재승 / 출판사_ 프로그래밍 인사이트 ]

    'JavaScript > JavaScript' 카테고리의 다른 글

    JavaScript | Webpack 기초 정리  (0) 2020.04.23
    JavaScript | Babel Plugin 제작하기  (0) 2020.04.22
    JavaScript | Babel 기초 정리  (0) 2020.04.22
    JavaScript | Event Handling 이벤트 처리  (0) 2020.04.09
    JavaScript | With문  (0) 2020.02.18

    댓글

Designed by Tistory.