Redux+TS 01 | 기본

Kate Jung·2022년 1월 2일
0

middlewares & libraries

목록 보기
11/17
post-thumbnail

📌 프로젝트 준비 (설치)

$ npx create-react-app [프로젝트 폴더명] --template typescript
$ cd [프로젝트 폴더명]
$ yarn add redux react-redux @types/react-redux

🔹 타입 스크립트 (자체적으로) 지원 여부

  • o

    • redux
  • x

    • react-redux

    • 대안

      @types/ 를앞에 붙여서 설치

  • 라이브러리의 타입스크립트 지원 여부 확인 방법

    • 직접 설치해서 불러와서 확인
    • GitHub 레포를 열어 index.d.ts 파일 유무 확인

🔹 @types

써드파티 라이브러리 (라이브러리에 타입스크립트 지원 가능하도록 추가)

  • 라이브러리의 써드 파티 타입스크립트 지원 여부 확인 방법

    • npm 에서 @types/라이브러리명 을 입력

    • TypeSearch 에서 라이브러리명을 검색

📌 Counter

🔹 리덕스 모듈 작성

◼ Ducks 패턴 사용

→ 액션타입, 액션생성함수, 리듀서를 모두 한 파일에 작성

◼ src/modules/counter.ts

// 액션 타입 선언
const INCREASE = "counter/INCREASE" as const;
const DECREASE = "counter/DECREASE" as const;
const INCREASE_BY = "counter/INCREASE_BY" as const;

// 액션 생성 함수 선언
export const increase = () => ({
  type: INCREASE,
});

export const decrease = () => ({
  type: DECREASE,
});

export const increaseBy = (diff: number) => ({
  type: INCREASE_BY,
  payload: diff,
});

// 모든 액션 객체들에 대한 타입 준비
type CounterAction =
  | ReturnType<typeof increase>
  | ReturnType<typeof decrease>
  | ReturnType<typeof increaseBy>;

// 이 리덕스 모듈에서 관리할 상태의 타입 선언
type CounterState = {
  count: number;
};

// 초기 상태 선언
const initialState: CounterState = {
  count: 0,
};

// 리듀서 작성
function counter(
  state: CounterState = initialState,
  action: CounterAction
): CounterState {
  switch (action.type) {
    case INCREASE: // case 입력 후, Ctrl + Space 를 누르면 어떤 종류의 action.type들이 있는지 확인 가능 
      return { count: state.count + 1 };
    case DECREASE:
      return { count: state.count - 1 };
    case INCREASE_BY:
      return { count: state.count + action.payload };
    default:
      return state;
  }
}

export default counter;

◼ 참고

🍀 1. [ 액션 타입 선언 ] as const

action.type이 실제 문자열로 추론 되도록 함.

추후 액션 객체 만들 때, action.type 의 값을 추론하는 과정에서
action.typestring이 아닌, 실제 문자열(ex. "counter/INCREASE")로 추론 되도록 함.

🍀 2. [ 액션 생성 함수 선언 ] payload

액션에 부가적으로 필요한 값

  • FSA 규칙 :

    이 규칙을 적용하면 액션들이 모두 비슷한 구조로 이루어지게 됨.

    → 추후 다룰 때 편함 + 읽기 쉬움 + (액션 구조 일반화 → 액션 관련 라이브러리 사용 가능)

  • 무조건 따를 필요 x

🍀 3. [ 모든 액션 객체들에 대한 타입 준비 ] ReturnType<typeof _____>

특정 함수의 반환값을 추론

  • 주의

    상단부의 액션 타입 선언 시, as const 를 하지 않으면 제대로 작동 x

🍀 4. [ 리듀서 작성 ] 주의 사항

  • 리듀서

    state와 함수의 반환값이 일치하도록 작성

  • 액션

    CounterAction 을 타입으로 설정

🔹 프로젝트에 리덕스 적용

◼ 루트 리듀서 만들기 (modules/index.ts)

import { combineReducers } from "redux";
import counter from "./counter";

const rootReducer = combineReducers({
  counter,
});

// 루트 리듀서 내보내기
export default rootReducer;

// 루트 리듀서의 반환값 유추
// 추후 이 타입을 컨테이너 컴포넌트에서 불러와서 사용해야 하므로 내보내줌.
export type RootState = ReturnType<typeof rootReducer>;

◼ 스토어 제작 및 적용 (index.tsx)

  1. index.tsx 에서 스토어 제작
  2. 스토어를 프로젝트에 적용 (Provider 컴포넌트 사용)
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './modules';

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

🔹 프레젠테이셔널 컴포넌트 제작

  • 주의

    리액트 컴포넌트 작성 시 → .tsx 확장자 사용

  • src/components/Counter.tsx

    import React from 'react';
    
    type CounterProps {
      count: number;
      onIncrease: () => void;
      onDecrease: () => void;
      onIncreaseBy: (diff: number) => void;
    }
    
    function Counter({
      count,
      onIncrease,
      onDecrease,
      onIncreaseBy
    }: CounterProps) {
      return (
        <div>
          <h1>{count}</h1>
          <button onClick={onIncrease}>+1</button>
          <button onClick={onDecrease}>-1</button>
          <button onClick={() => onIncreaseBy(5)}>+5</button>
        </div>
      );
    }
    
    export default Counter;

🔹 컨테이너 컴포넌트 제작

리덕스의 값 불러와서 사용, 액션도 디스패치함.

  • src/containers/CounterContainer.tsx
    import React from "react";
    import { useSelector, useDispatch } from "react-redux";
    import { RootState } from "../modules";
    import { increase, decrease, increaseBy } from "../modules/counter";
    import Counter from "../components/Counter";
    
    function CounterContainer() {
      // 상태 조회 (상태 조회 시, state의 타입을 RootState로 지정)
      const count = useSelector((state: RootState) => state.counter.count);
      const dispatch = useDispatch(); // 디스패치 함수 가져옴
    
      // 각 액션들을 디스패치하는 함수 제작
      const onIncrease = () => {
        dispatch(increase());
      };
    
      const onDecrease = () => {
        dispatch(decrease());
      };
    
      const onIncreaseBy = (diff: number) => {
        dispatch(increaseBy(diff));
      };
    
      return (
        <Counter
          count={count}
          onIncrease={onIncrease}
          onDecrease={onDecrease}
          onIncreaseBy={onIncreaseBy}
        />
      );
    }
    
    export default CounterContainer;
  • count 값의 타입

    useSelector 가 (알아서) 유추

    → 굳이 :number 라고 타입을 설정 할 필요 x

🔹 작동 확인

  1. App에서 CounterContainer 렌더링 (App.tsx)
import React from "react";
import CounterContainer from "./containers/CounterContainer";

const App: React.FC = () => {
  return <CounterContainer />;
};

export default App;
  1. 개발 서버 구동 (yarn start)

📌 Todolist

🔹 리덕스 모듈 작성

// **src/modules/todos.ts**

// 액션 타입 선언
const ADD_TODO = "todos/ADD_TODO" as const;
const TOGGLE_TODO = "todos/TOGGLE_TODO" as const;
const REMOVE_TODO = "todos/REMOVE_TODO" as const;

// 새로운 항목을 추가 할 때 사용 할 고유 ID 값
let todosId = 0;

// 액션 생성 함수
export const addTodo = (text: string) => ({
  type: ADD_TODO,
  payload: {
    id: todosId++,
    text,
  },
});

export const toggleTodo = (id: number) => ({
  type: TOGGLE_TODO,
  payload: id,
});

export const removeTodo = (id: number) => ({
  type: REMOVE_TODO,
  payload: id,
});

// 모든 액션 객체들에 대한 타입
type TodosAction =
  | ReturnType<typeof addTodo>
  | ReturnType<typeof toggleTodo>
  | ReturnType<typeof removeTodo>;

// 투두 타입
export type Todo = {
  id: number;
  text: string;
  done: boolean;
};

// 상태 타입 (전체 투두)
export type TodosState = Todo[];

// 초기 상태
const initialState: TodosState = [];

// 리듀서
function todos(
  state: TodosState = initialState,
  action: TodosAction
): TodosState {
  switch (action.type) {
    case ADD_TODO:
      return state.concat({
        // action.payload 객체 안의 값이 모두 유추됨.
        id: action.payload.id,
        text: action.payload.text,
        done: false,
      });
    case TOGGLE_TODO:
      return state.map((todo) =>
        // payload 가 number 인 것이 유추됨.
        todo.id === action.payload ? { ...todo, done: !todo.done } : todo
      );
    case REMOVE_TODO:
      // payload 가 number 인 것이 유추됨.
      return state.filter((todo) => todo.id !== action.payload);
    default:
      return state;
  }
}

export default todos;

🔹 루트 리듀서에 todos 리듀서 등록

// modules/index.ts

import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

const rootReducer = combineReducers({
  counter,
  todos
});

// 루트 리듀서를 내보내주세요.
export default rootReducer;

// 루트 리듀서의 반환값를 유추해줍니다
// 추후 이 타입을 컨테이너 컴포넌트에서 불러와서 사용해야 하므로 내보내줍니다.
export type RootState = ReturnType<typeof rootReducer>;

🔹 프리젠테이셔널 컴포넌트 준비

만들 프리젠테이셔널 컴포넌트

  • TodoInsert : 새 항목 등록용
  • TodoItem : 할 일 정보를 보여주는 용
  • TodoList : 여러 개의 TodoItem 을 렌더링하는 용

src/components/TodoInsert.tsx

  • onInsert

    props (함수) 를 받아와 호출하여 새 항목 추가

  • input 의 상태

    컴포넌트 내부에서 로컬 상태로 관리

import React, { ChangeEvent, FormEvent, useState } from "react";

type TodoInsertProps = {
  onInsert: (text: string) => void;
};

function TodoInsert({ onInsert }: TodoInsertProps) {
  const [value, setValue] = useState("");

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };

  const onSubmit = (e: FormEvent) => {
    e.preventDefault();
    onInsert(value);
    setValue("");
  };

  return (
    <form onSubmit={onSubmit}>
      <input
        placeholder="할 일을 입력하세요."
        value={value}
        onChange={onChange}
      />
      <button type="submit">등록</button>
    </form>
  );
}

export default TodoInsert;

src/components/TodoItem.tsx

각 할 일 항목에 대한 정보를 보여주는 컴포넌트

  • 텍스트 영역 클릭 시 → done 값이 바뀜
  • 우측의 (X) 클릭 시 → 항목 삭제

  • props 로 받아오는 것
    • todo : 할 일 정보
    • onToggle & onRemove : 상태 토글 및 삭제를 해주는 함수

  • CSSProperties 이란?

    style 객체의 타입


  • 코드
    import React, { CSSProperties } from "react";
    import { Todo } from "../modules/todos";
    
    type TodoItemProps = {
      todo: Todo;
      onToggle: (id: number) => void;
      onRemove: (id: number) => void;
    };
    
    function TodoItem({ todo, onToggle, onRemove }: TodoItemProps) {
      const textStyle: CSSProperties = {
        textDecoration: todo.done ? "line-through" : "none",
      };
      const removeStyle: CSSProperties = {
        marginLeft: 8,
        color: "red",
      };
    
      const handleToggle = () => {
        onToggle(todo.id);
      };
    
      const handleRemove = () => {
        onRemove(todo.id);
      };
    
      return (
        <li>
          <span onClick={handleToggle} style={textStyle}>
            {todo.text}
          </span>
          <span onClick={handleRemove} style={removeStyle}>
            (X)
          </span>
        </li>
      );
    }
    
    export default TodoItem;

src/components/TodoList.tsx

여러 개의 TodoItem 컴포넌트를 렌더링

  • props 로 받아오는 것

    • todo : 할 일 정보

    • onToggle & onRemove : TodoItem 컴포넌트들에게 전달

  • 코드

    import React from 'react';
    import { Todo } from '../modules/todos';
    import TodoItem from './TodoItem';
    
    type TodoListProps = {
      todos: Todo[];
      onToggle: (id: number) => void;
      onRemove: (id: number) => void;
    };
    
    function TodoList({ todos, onToggle, onRemove }: TodoListProps) {
      if (todos.length === 0) return <p>등록된 항목이 없습니다.</p>;
      return (
        <ul>
          {todos.map(todo => (
            <TodoItem
              todo={todo}
              onToggle={onToggle}
              onRemove={onRemove}
              key={todo.id}
            />
          ))}
        </ul>
      );
    }
    
    export default TodoList;

🔹 컨테이너 컴포넌트 제작

// src/containers/TodoApp.tsx

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../modules";
import { toggleTodo, removeTodo, addTodo } from "../modules/todos";
import TodoInsert from "../components/TodoInsert";
import TodoList from "../components/TodoList";

function TodoApp() {
  const todos = useSelector((state: RootState) => state.todos);
  const dispatch = useDispatch();

  const onInsert = (text: string) => {
    dispatch(addTodo(text));
  };

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

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

  return (
    <>
      <TodoInsert onInsert={onInsert} />
      <TodoList todos={todos} onToggle={onToggle} onRemove={onRemove} />
    </>
  );
}

export default TodoApp;

🔹 렌더링

// src/App.tsx

import React from 'react';
import TodoApp from './containers/TodoApp';

const App: React.FC = () => {
  return <TodoApp />;
};

export default App;

참고

profile
복습 목적 블로그 입니다.

0개의 댓글