[React] Redux 더 편하게 사용하기

UkiUkhui·2021년 11월 26일
0

React 공부중

목록 보기
25/25

1. redux-action

1.1. counter 모듈에 적용하기

modules/counter

import { createAction } from "redux-action";

const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
//액션 타입 정의

// export const increase = () => ({ type: INCREASE });
// export const decrease = () => ({ type: DECREASE });
//액션 생성함수 정의

export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

createAction : 액션생성 함수를 더 짧게 정의가능

  • 매번 객체를 만들어줄 필요 없이 더욱 간단하게 액션 생성함수를 정의
const initialState = {
  number: 0
};

// function counter(state = initialState, action) {
//   switch (action.type) {
//     case INCREASE:
//       return {
//         number: state.number + 1
//       };
//     case DECREASE:
//       return {
//         number: state.number - 1
//       };
//     default:
//       return state;
//   }
// }
// 리듀서 함수 정의

const counter = handleActions(
  {
    [INCREASE]: (state, action) => ({ number: state.number + 1 }),
    [DECREASE]: (state, action) => ({ number: state.number - 1 })
  },
  initialState
);

handleActions : 첫 번째 함수에 각 액션에 대한 업데이트 함수, 두 번째 파라미터에는 초깃값

  • switch/case 사용하지 않음

1.2. todos 모듈에 적용하기

  • 각 액션함수는 파라미터를 필요로 함
  • createAction으로 액션 생성함수를 만들면 액션에 필요한 추가데이터는 payload라는 이름을 사용
const MY_ACTION = 'sample/MY_ACTION';
const myAction = createAction(MY_ACTION);
const action = myAction('hello world');
//결과: { type: MY_ACTION, payload: 'hello world' }
  • 액션 생성함수에서 받아온 파라미터를 그대로 payload에 넣는 것이 아니라 다른 형태로 넣고 싶다면, createAction의 두번째 파라미터에 payload를 정의하는 함수를 넣으면 됨

modules/todos

import { createAction } from "redux-action";

const CHANGE_INPUT = "todos/CHANGE_INPUT";
const INSERT = "todos/INSERT";
const TOGGLE = "todos/TOGGLE";
const REMOVE = "todos/REMOVE";

// export const changeInput = (input) => ({
//   type: CHANGE_INPUT,
//   input
// });

// let id = 3;
// export const insert = (text) => ({
//   type: INSERT,
//   todo: {
//     id: id++,
//     text,
//     done: false
//   }
// });

// export const toggle = (id) => ({
//   type: TOGGLE,
//   id
// });

// export const remove = (id) => ({
//   type: REMOVE,
//   id
// });

export const changeInput = createAction(CHANGE_INPUT, (input) => input);
let id = 3;
export const insert = createAction(INSERT, (text) => ({
  id: id++,
  text,
  done: false
}));
export const toggle = createAction(TOGGLE, (id) => id);
export const remove = createAction(REMOVE, (id) => id);
  • insert의 경우, text를 넣으면 todo 객체를 반환
  • 그 외 text=>text, id=>id 는 넣으나 안 넣으나 동일하게 작동하지만, 명시적으로 액션생성함수의 파라미터로 무엇을 필요로 하는지 알 수 있게 함

action.payload : 액션에 필요한 추가 데이터를 모두 payload라는 이름 사용하므로, 공통적으로 action.payload값을 조회하도록 리듀서 구현해야 함

  • createAction로 만든 액션 생성함수는 파라미터로 받아온 값을 객체안에 넣을 때 action.id, action.input 같이 action.payload를 공통적으로 넣어줘야 함
  • 업데이트 로직에서 action.payload값을 조회하여 업데이트하도록 구현
// function todos(state = initialState, action) {
//   switch (action.type) {
//     case CHANGE_INPUT:
//       return {
//         ...state,
//         input: action.input
//       };
//     case INSERT:
//       return {
//         ...state,
//         todos: state.todos.concat(action.todo)
//       };
//     case TOGGLE:
//       return {
//         ...state,
//         todos: state.todos.map((todo) =>
//           todo.id === action.id
//             ? {
//                 ...todo,
//                 done: !todo.done
//               }
//             : todo
//         )
//       };
//     case REMOVE:
//       return {
//         ...state,
//         todos: state.todos.filter((todo) => todo.id !== action.id)
//       };
//     default:
//       return state;
//   }
// }

const todos = handleActions(
  {
    [CHANGE_INPUT]: (state, action) => ({ ...state, input: action.payload }),
    [INSERT]: (state, action) => ({
      ...state,
      todos: state.todos.concat(action.payload)
    }),
    [TOGGLE]: (state, action) => ({
      ...state,
      todos: state.todos.map((todo) =>
        todo.id === action.payload ? { ...todo, done: !todo.done } : todo
      )
    }),
    [REMOVE]: (state, action) => ({
      ...state,
      todos: state.todos.filter((todo) => todo.id !== action.id)
    })
  },
  initialState
);

export default todos;

1.2.1. payload 객체 비구조화 할당

  • 모든 추가 데이터 값을 action.payload로 사용하면 헷갈릴 수 있음
    • 객체 비구조화 할당을 통해 action값의 payload 이름 재설정하기
const todos = handleActions({
    [CHANGE_INPUT]: (state, { payload: input }) => ({ ...state, input }),
    [INSERT]: (state, { payload: todo }) => ({ ...state, todos: state.todos.concat(todo) }),
    [TOGGLE]: (state, { payload: id }) => ({
        ...state,
        todos: state.todos.map((todo) => todo.id === id ? { ...todo, done: !todo.done } : todo)
    }),
    [REMOVE]: (state, { payload: id }) => ({
        ...state,
        todos: state.todos.filter((todo) => todo.id !== id)
    })
}, initialState);

2. immer

  • 모듈 상태가 복잡해질 수록 불변성 지키기가 점점 어려움
  • 객체의 구조가 복잡하거나 객체로 이루어진 배열 다룰 때, immer 사용하면 편리함.

yarn add immer

const deepObject = {
    modal: {
        open: false,
        content: {
            title: '알림',
            body: '성공',
            buttons: {
                confirm: '확인',
                cancel: '취소',
            },
        },
    },
    waiting: false,
    settings: {
        theme: 'dark',
        zoomLevel: 5,
    },
};


const shallowObject = {
    modal: {
        open: false,
        title: '알림',
        body: '성공',
        confirm: '확인',
        cancel: '취소',
    },
    waiting: false,
    theme: 'dark',
    zoomLevel: 5
}

const todos = handleActions({
    [CHANGE_INPUT]: (state, { payload: input }) =>
        produce(state, draft => {
            draft.input = input;
        }),
    [INSERT]: (state, { payload: todo }) =>
        produce(state, draft => {
            draft.todos.push(todo);
        }),
    [TOGGLE]: (state, { payload: id }) =>
        produce(state, draft => {
            const todo = draft.todos.find(todo => todo.id === id);
            todo.done = !todo.done;
        }),
    [REMOVE]: (state, { payload: id }) =>
        produce(state, draft => {
            const index = draft.todos.findIndex(todo => todo.id === id);
            draft.todos.splice(index, 1);
        }),
}, initialState)

3. Hook 사용하여 컨테이너 컴포넌트 사용하기

  • 리덕스와 연동된 컨테이너 컴포넌트 만들 때 connect 함수 대신 훅을 사용

3.1. useSelector

  • connect 사용하지 않고도 리덕스의 상태 조회 가능.

const 결과 = useSelector(상태 선택 함수);

  • 상태 선택 함수 : mapStateToProps와 동일함
import { connect, useSelector } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";

// const CounterContainer = ({ number, increase, decrease }) => {
//     return (
//         <Counter number={number} onIncrease={increase} onDecrease={decrease} />
//     );
// };

// export default connect(
//     (state) => ({
//         number: state.counter.number
//     }),
//     (dispatch) => ({
//         increase: () => {
//             dispatch(increase());
//         },
//         decrease: () => {
//             dispatch(decrease());
//         }
//     })
// )(CounterContainer);

const CounterContainer = () => {
    const number = useSelector(state => state.counter.number);
    return(<Counter number={number} />);
}

export default CounterContainer;
  • useSelector를 통해 counter.number 조회

3.2. useDispatch

const dispatch = useDispatch();
dispatch({ type: 'SAMPLE_ACTION' });

  • 스토어의 내장함수인 dispatch를 사용할 수 있게 해줌
  • 컨테이너 컴포넌트에서 액션을 dispatch할 때 사용

containers/CounterContainer

import { connect, useDispatch, useSelector } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";

// const CounterContainer = ({ number, increase, decrease }) => {
//     return (
//         <Counter number={number} onIncrease={increase} onDecrease={decrease} />
//     );
// };

// export default connect(
//     (state) => ({
//         number: state.counter.number
//     }),
//     (dispatch) => ({
//         increase: () => {
//             dispatch(increase());
//         },
//         decrease: () => {
//             dispatch(decrease());
//         }
//     })
// )(CounterContainer);

const CounterContainer = () => {
    const number = useSelector(state => state.counter.number);
    const dispatch = useDispatch();
    return (<Counter number={number}
        onIncrease={() => dispatch(increase())}
        onDecrease={() => dispatch(decrease())}
    />);
}

export default CounterContainer;
  • 코드 최적화 : useCallback으로 감싸서 컴포넌트 리렌더링되더라도 새로 함수를 만들지 않게 함.
const CounterContainer = () => {
    const number = useSelector(state => state.counter.number);
    const dispatch = useDispatch();
    const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
    const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
    return (<Counter number={number}
        onIncrease={onIncrease}
        onDecrease={onDecrease}
    />);
}

containers/TodosContainer

import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import Todos from "../components/Todos";
import { changeInput, insert, toggle, remove } from "../modules/todos";

// const TodosContainer = ({
//     input,
//     todos,
//     changeInput,
//     insert,
//     toggle,
//     remove
// }) => {
//     return (
//         <Todos
//             input={input}
//             todos={todos}
//             onChangeInput={changeInput}
//             onInsert={insert}
//             onToggle={toggle}
//             onRemove={remove}
//         />
//     );
// };

// export default connect(
//     ({ todos }) => ({
//         input: todos.input,
//         todos: todos.todos
//     }),
//     {
//         changeInput,
//         insert,
//         toggle,
//         remove
//     }
// )(TodosContainer);

const TodosContainer = () => {
    const { input, todos } = useSelector(({ todos }) => ({
        input: todos.input,
        todos: todos.todos
    }));
    const dispatch = useDispatch();
    const onChangeInput = useCallback(input => dispatch(changeInput(input)), [dispatch]);
    const onInsert = useCallback(text => dispatch(insert(text)), [dispatch]);
    const onToggle = useCallback(id => dispatch(toggle(id)), [dispatch]);
    const onRemove = useCallback(id => dispatch(remove(id)), [dispatch]);

    return (
        <Todos
            input={input}
            todos={todos}
            onChangeInput={onChangeInput}
            onInsert={onInsert}
            onToggle={onToggle}
            onRemove={onRemove}
        />)
}

export default TodosContainer;

3.3. useStore

  • 컴포넌트 내부에서 리덕스 스토어 객체 직접 사용 가능

const store = useStore();
store.dispatch({ type: 'SAMPLE_ACTION' });
store.getState();

3.4. useActions

  • 여러 개의 액션을 사용하는 경우 코드를 더 깔끔하게 만들 수 있음
  • 액션 생성 함수를 액션을 디스패치하는 함수로 변환
  • 액션 생성 함수를 사용하여 액션 객체 만들고, 이를 스토어에 디스패치해주는 작업을 함수를 통해 자동화함

lib/useActions

import { useMemo } from "react";
import { useDispatch } from "react-redux";
import { bindActionCreators } from "redux";

export default function useActions(actions, deps) {
    const dispatch = useDispatch();
    return useMemo(
        () => {
            if (Array.isArray(actions)) {
                return actions.map(a => bindActionCreators(a, dispatch));
            }
            return bindActionCreators(actions, dispatch);
        },
        deps ? [dispatch, ...deps] : deps
    );
}
  • 첫번째 파라미터 : 액션 생성 함수로 이루어진 배열
  • 두번째 파라미터 : deps 배열로, 이 배열 안에 있는 원소가 바뀌면 액션을 디스패치하는 함수를 만듦

containers/TodosContainer

import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import Todos from "../components/Todos";
import useActions from "../lib/useActions";
import { changeInput, insert, toggle, remove } from "../modules/todos";

// const TodosContainer = ({
//     input,
//     todos,
//     changeInput,
//     insert,
//     toggle,
//     remove
// }) => {
//     return (
//         <Todos
//             input={input}
//             todos={todos}
//             onChangeInput={changeInput}
//             onInsert={insert}
//             onToggle={toggle}
//             onRemove={remove}
//         />
//     );
// };

// export default connect(
//     ({ todos }) => ({
//         input: todos.input,
//         todos: todos.todos
//     }),
//     {
//         changeInput,
//         insert,
//         toggle,
//         remove
//     }
// )(TodosContainer);

const TodosContainer = () => {
    const { input, todos } = useSelector(({ todos }) => ({
        input: todos.input,
        todos: todos.todos
    }));
    // const dispatch = useDispatch();
    // const onChangeInput = useCallback(input => dispatch(changeInput(input)), [dispatch]);
    // const onInsert = useCallback(text => dispatch(insert(text)), [dispatch]);
    // const onToggle = useCallback(id => dispatch(toggle(id)), [dispatch]);
    // const onRemove = useCallback(id => dispatch(remove(id)), [dispatch]);

    const [onChangeInput, onInsert, onToggle, onRemove] = useActions(
        [changeInput, insert, toggle, remove],
        []
    );
    return (
        <Todos
            input={input}
            todos={todos}
            onChangeInput={onChangeInput}
            onInsert={onInsert}
            onToggle={onToggle}
            onRemove={onRemove}
        />)
}

export default TodosContainer;

4. connect와의 차이점

  • connect : 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링되어도 props가 변경되지 않았다면 재렌더링 되지 않음(성능 최적화)

  • Hook : 성능 최적화를 위해 React.memo()사용

profile
hello world!

0개의 댓글