순서

  1. 리덕스 리듀서 모듈 생성(Ducks Pattern) - 한 파일에 액션, 액션 생성 함수, 리듀서를 작성하는 방식
  2. 각 리듀서 모듈에서 액션타입(TS에서의 타입이 아님), 액션 생성 함수, 액션 객체 타입 선언(TS에서의 타입), 초기값, 기본값 타입들, 리듀서를 작성한다.
  3. '루트 리듀서'역할을 하는 index.ts를 만들어, 리덕스의 combineReducers를 이용해 만든 리듀서 모듈들을 하나로 통합해 export 한다.
  4. 프로젝트 전체에 리듀서를 적용하고, 스토어에 접근 가능하도록 앱의 최상위 파일에서 store를 생성해 Provider를 이용해 적용시킨다.
  5. 필요한 컴포넌트들을 작성한다. 여기서, useSelector , useDispatch 를 이용해 각각 조금 더 쉽게 리듀서를 연결하고 스토어에 접근해 값을 사용하고, 액션을 발생시켜 상태를 변화시킨다.

 

리듀서 모듈 생성

리덕스 사용에 필요한 액션, 액션 생성함수, 리듀서를 한 파일에 작성한다. 이러한 방식을 'Ducks 패턴'이라 한다.

우선, 필요한 액션 타입을 상수로 설정한다.

const ADD_TODO = "todo/ADD_TODO" as const;
const REMOVE_TODO = "todo/REMOVE_TODO" as const;
const TOGGLE_TODO = "todo/TOGGLE_TODO" as const;

 

위와 같이 타입스크립트 문법인 Type Assertions(타입 단언)을 사용해 액션 타입에 대한 타입 추론의 범위를 줄여 string 타입이 아닌 명확히 액션 타입을 추론하도록 해준다. 액션 타입들을 상수화 시켜두면 추후에 코드 수정에 있어 용이해진다.

 

 

다음으로 액션 생성 함수를 작성한다. 모듈 외부에서 사용할 수 있도록 export 해준다. Context api나 Redux 에서 액션을 dispatch를 이용해 액션을 발생시킬 때 해당 액션 생성 함수들을 사용한다.

// Action Creators
export const addTodo = (text: string) => ({ type: ADD_TODO, payload: text });
export const removeTodo = (id: number) => ({ type: REMOVE_TODO, payload: id });
export const toggleTodo = (id: number) => ({ type: TOGGLE_TODO, payload: id });

 

다음으로 액션 객체에 대한 타입들을 설정해줘야 한다.

// 액션 객체 타입 설정
type TodoAction =
  | ReturnType<typeof addTodo>
  | ReturnType<typeof removeTodo>
  | ReturnType<typeof toggleTodo>;

 

ReturnType<T> 는 유틸리티 타입의 한 종류로서 전역적으로 사용 가능하다. 기능으로는 함수 T반환 타입으로 구성된 타입을 만들어준다. 아래에 간단한 예시를 살펴보자.

declare function f1(): { a: number, b: string }
type T0 = ReturnType<() => string>;  // string
type T1 = ReturnType<(s: string) => void>;  // void
type T2 = ReturnType<(<T>() => T)>;  // {}
type T3 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T4 = ReturnType<typeof f1>;  // { a: number, b: string }
type T5 = ReturnType<any>;  // any
type T6 = ReturnType<never>;  // any
type T7 = ReturnType<string>;  // 오류
type T8 = ReturnType<Function>;  // 오류

 

ReturnType으로 인한 위의 액션 객체의 타입은 다음과 같이 될 것이다. 앞서 Type Assertions으로 const로 지정을 해주지 않았다면 제대로 동작하지 않을 수도 있다.

 

 

다음으로 리듀서 함수에 전달 할 state에 대해 필요한 타입과 초기값들을 지정해준다.

export type Todo = {
  id: number;
  text: string;
  isToggle: boolean;
};

export type Todos = Todo[];

export const initialState: Todos = [
  {
    id: 0,
    text: "타입스크립트 투두리스트",
    isToggle: false
  },
  {
    id: 1,
    text: "타입스크립트 리덕스",
    isToggle: false
  }
];

 

마지막으로 앞서 작성한 state와 action 객체에 대한 타입을 이용해 리듀서 함수를 작성한다.

// Reducer
export default function todoReducer(state: Todos = initialState, action: TodoAction): Todos {
  switch (action.type) {
    case ADD_TODO:
      const id = Math.max(...state.map((todo) => todo.id)) + 1; // 현재 있는 리스트 중 가장 큰 id값에 +1 한다.

      return state.concat({
        id,
        text: action.payload,
        isToggle: false
      });

    case REMOVE_TODO:
      return state.filter((todo) => todo.id !== action.payload);

    case TOGGLE_TODO:
      return state.map((todo) =>
        todo.id === action.payload
          ? { ...todo, isToggle: !todo.isToggle }
          : todo
      );

    default:
      return state;
  }
}

 

최종적인 리듀서 모듈의 코드는 다음과 같다.

// src/modules/todo.ts


// Action Type
const ADD_TODO = "todo/ADD_TODO" as const; // Type Assertions(현재 나타내는 타입보다 더 구체적인 타입을 나타내려 할 때 이용)
const REMOVE_TODO = "todo/REMOVE_TODO" as const;
const TOGGLE_TODO = "todo/TOGGLE_TODO" as const;

// Action Creators
export const addTodo = (text: string) => ({ type: ADD_TODO, payload: text });
export const removeTodo = (id: number) => ({ type: REMOVE_TODO, payload: id });
export const toggleTodo = (id: number) => ({ type: TOGGLE_TODO, payload: id });

// 액션 객체 타입 설정
// ReturnType --> 타입스크립트의 특정함수의 반환 타입을 추출해내는 제네릭 타입으로
// 이를 통해 interface 중복작성을 피할 수 있다.
type TodoAction =
  | ReturnType<typeof addTodo>
  | ReturnType<typeof removeTodo>
  | ReturnType<typeof toggleTodo>;

//---- 리듀서에 전달 할 state에 대한 처리 ----
export type Todo = {
  id: number;
  text: string;
  isToggle: boolean;
};

export type Todos = Todo[];

export const initialState: Todos = [
  {
    id: 0,
    text: "타입스크립트 투두리스트",
    isToggle: false
  },
  {
    id: 1,
    text: "타입스크립트 리덕스",
    isToggle: false
  }
];

// ------------------------------------

// Reducer
export default function todoReducer(
  state: Todos = initialState,
  action: TodoAction
): Todos {
  switch (action.type) {
    case ADD_TODO:
      const id = Math.max(...state.map((todo) => todo.id)) + 1;
      return state.concat({
        id,
        text: action.payload,
        isToggle: false
      });

    case REMOVE_TODO:
      return state.filter((todo) => todo.id !== action.payload);

    case TOGGLE_TODO:
      return state.map((todo) =>
        todo.id === action.payload
          ? { ...todo, isToggle: !todo.isToggle }
          : todo
      );

    default:
      return state;
  }
}

 

 

Root reducer 만들기

리덕스의 combineReducers를 이용해 만든 리듀서 모듈들을 하나로 통합해 만든 루트 리듀서 파일을 export 한다.

// src/modules/index.ts

import { combineReducers } from "redux";
import todoReducer from "./todo";

const rootReducer = combineReducers({
  // 여러 리듀서 모듈들을 하나로 병합한다.
  todoReducer
});

export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;

 

 

최상위 컴포넌트 Provider 감싸기

최상위 컴포넌트 파일인  루트의 index.tsx 파일에서 <Provider> 컴포넌트로 하위 모든 컴포넌트에서 store에 접근이 가능하게 해 준다. 

// ./index.tsx

import { render } from "react-dom";
import React from "react";
import { Provider } from "react-redux";
import { createStore } from "redux";

import rootReducer from "./modules";
import App from "./App";

const store = createStore(rootReducer); // store 생성

const rootElement = document.getElementById("root");
render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);

 

필요한 컴포넌트들 작성

Redux를 사용하기 위한 준비를 다 마쳤다. 이제 컴포넌트들을 작성하고 Redux 기능들을 활용하면 된다.

src 폴더 안에 components폴더를 만들고, TodoApp.tsx 와 TodoAppContianer.tsx파일을 만든다. 각각의 코드는 다음과 같다.

 

// /components/TodoApp.tsx

import React, { useState } from "react";
import { Todos, Todo } from "../modules/todo";
import { useDispatch } from "react-redux";
import { addTodo, toggleTodo, removeTodo } from "../modules/todo";

type Props = {
  todos: Todos;
};

const TodoApp = ({ todos }: Props) => {
  // 디스패치 함수에 파라미터로 액션객체를 넣어주면 스토어로 전달하여 액션을 발생시키고,
  // 리듀서 모듈에 이 액션이 있다면 새로운 상태로 바뀌게 되는것
  const dispatch = useDispatch();
  const [input, setInput] = useState("");

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    dispatch(addTodo(input));
    setInput("");
  };

  const handleClick = (id: number) => {
    dispatch(toggleTodo(id));
  };

  const handleRemove = (id: number) => {
    dispatch(removeTodo(id));
  };

  const done = {
    textDecoration: "line-through"
  };

  return (
    <>
      <div>
        <form onSubmit={(e) => handleSubmit(e)}>
          <input value={input} onChange={(e) => setInput(e.target.value)} />
          <button type="submit">등록</button>
        </form>
      </div>
      <div>
        {todos.map((todo) => {
          const { id, text, isToggle } = todo;
          return (
            <div
              key={id}
              onClick={() => handleClick(id)}
              style={isToggle ? done : undefined}
            >
              {text}
              <button onClick={() => handleRemove(id)}>삭제</button>
            </div>
          );
        })}
      </div>
    </>
  );
};

export default TodoApp;

 

위의 코드에서 dispatch 함수의 인자로 '액션 생성함수(action creator)'를 넣어줬다. 즉, modules 폴더에서 작성한 리듀서 파일 안의 액션 생성 함수들의 결과로 '액션 객체' 값을 리턴 받아 dispatch 하게 된다. 그런 다음 reducer에서 액션 타입에 맞는 로직을 수행하게 되고 store의 상태 값을 변화시키게 된다.

 

// /components/TodoAppContainer.tsx

import React from "react";
import TodoApp from "./TodoApp";
import { useSelector } from "react-redux";
import { RootState } from "../modules";

const TodoAppContainer = () => {
  // useSelector :: 간단하게 리듀서 모듈과 연결해 데이터를 받아올 수 있다.
  const todos = useSelector((state: RootState) => state.todoReducer);
  return <TodoApp todos={todos} />;
};

export default TodoAppContainer;

useSelector는connect 를 통해 상태 값을 조회하는 것보다 훨씬 간결하게 작성하고 코드 가독성이 상승되는 장점이 있는 함수다.

 

최종적으로 다음과 같이 나타나게된다.

 

반응형

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

[Redux] Redux 개념 익히기  (0) 2021.02.28

+ Recent posts