DEVELOP
article thumbnail

도서 리액트를 다루는 기술 | 김민준 을 읽고 작성한 게시글입니다.

 

리액트를 다루는 기술(개정판)

개발은 언제나 즐겁고 재밌어야 한다는 생각을 갖고 있는 개발자이며, IT 기술을 가르치는 것을 굉장히 좋아하는 교육자이다. 또한, 사용자를 행복하게 만드는 서비스를 만드는 것이 가장 중요

books.google.co.kr


리덕스를 사용하는 이유

  • 소규모 프로젝트에서는 컴포넌트가 가진 state를 사용하는 것만으로도 충분하지만, 프로젝트의 규모가 커짐에 따라 상태 관리가 번거로워질 수 있다.
  • 리덕스를 사용하면, 상태 업데이트에 관한 로직을 모듈로 따로 분리하여 컴포넌트 파일과 별개로 관리할 수 있으므로 코드를 유지 보수하는 데 도움이 된다.
  • 또한, 여러 컴포넌트에서 동일한 상태를 공유해야 할 때 매우 유용하며, 실제 업데이트가 필요한 컴포넌트만 리렌더링되도록 쉽게 최적화해 줄 수도 있다.

react-redux 설치

yarn add redux react-redux

리덕스 사용하기

UI 준비하기

  • 리액트 프로젝트에서 리덕스를 사용할 때 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것이다.
  • 프레젠테이셔널 컴포넌트란 주로 상태 관리가 이루어지지 않고, 그저 props를 받아와서 화면에 UI를 보여 주기만 하는 컴포넌트를 말한다.
  • 컨테이너 컴포넌트는 리덕스와 연동되어 있는 컴포넌트로, 리덕스로부터 상태를 받아오기도 하고, 리덕스 스토어에 액션을 디스패치하기도 한다.
  • 이러한 패턴은 리덕스를 사용하는 데 필수 사항은 아니다.
  • 다만, 이 패턴을 사용하면 코드의 재사용성도 높아지고, 관심사의 분리가 이루어져 UI를 작성할 때 좀 더 집중할 수 있다.

카운터 컴포넌트 UI 만들기

// src/components/Counter.tsx

const Counter: React.FC<{
  count: number;
  onIncrease?: () => void;
  onDecrease?: () => void;
}> = ({ count, onIncrease, onDecrease }) => {
  return (
    <div>
      <div>
        <h1>{count}</h1>
        <button onClick={onIncrease}>+1</button>
        <button onClick={onDecrease}>-1</button>
      </div>
    </div>
  );
};

export default Counter;

할 일 목록 컴포넌트 UI 만들기

  • TodoItem 컴포넌트를 따로 분리한다.
// src/components/TodoItem.tsx

import Todo from "../types/Todo";
import "./TodoItem.css";

const TodoItem: React.FC<{
  todo: Todo;
  onToggle: (id: number) => void;
  onRemove: (id: number) => void;
}> = ({ todo, onToggle, onRemove }) => {
  return (
    <div className={`TodoItem ${todo.done ? "done" : ""}`}>
      <input type="checkbox" onChange={() => onToggle(todo.id)} />
      <span className="text">{todo.text}</span>
      <button className="remove" onClick={() => onRemove(todo.id)}>
        X
      </button>
    </div>
  );
};

export default TodoItem;
/* src/TodoItem.css */

.TodoItem.done .text {
  text-decoration: line-through;
  color: gray;
}

.TodoItem .remove {
  color: red;
  border: none;
  background: none;
}


```ts
// src/components/Todos.tsx
import Todo from "../types/Todo";
import TodoItem from "./TodoItem";

const Todos: React.FC<{
  todos: Todo[];
  onInsert: (text: string) => void;
  onToggle: (id: number) => void;
  onRemove: (id: number) => void;
}> = ({ todos, onInsert, onToggle, onRemove }) => {
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  };

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input />
        <button type="submit">등록</button>
      </form>
      <div>
        <TodoItem todo="할 일" />
        <TodoItem todo="할 일" />
        <TodoItem todo="할 일" />
      </div>
    </div>
  );
};

export default Todos;
function App() {
  return (
    <div className="App">
      <Counter count={0} />
      <hr />
      <Todos />
    </div>
  );
}

리덕스 관련 코드 작성하기

  • 리덕스를 사용할 땐 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 한다.
  • 이 코드들을 각각 다른 파일에 작성하는 방법도 있고, 기능별로 묶어서 파일 하나에 적성하는 방법도 있다.
  • 왼쪽 그림은 가장 일반적인 구조로서, actions, constants, reducers라는 세 개의 디렉터리를 만들고 그 안에 기능별로 파일을 하나씩 만드는 방식이다.
    • 코드를 종류에 따라 다른 파일에 작성하여 정리할 수 있어서 편리하지만, 새로운 액션을 만들 때마다 세 종류의 파일을 모두 수정해야 해서 불편함이 잇다.
    • 이 방식은 리덕스 공식 문서에서도 사용되므로 가장 기본적이라고 할 수 있지만, 사람에 따라 불편할 수도 있는 구조이다.
  • 오른쪽 그림은 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 다 작성하는 방식이다.
    • ⇒ 이런 방식을 Ducks 패턴이라고 부르며, 앞서 설명한 일반적인 구조로 리덕스를 사용하다가 불편함을 느낀 개발자들이 자주 사용한다.
  • 리덕스 관련 코드에 대한 디렉터리 구조는 정해진 방법이 없기 때문에 마음대로 작성해도 되지만, 위 두가지 방법이 주로 사용된다.

일반적인 구조 / Ducks 패턴

액션 타입 정의하기

  • Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 ‘모듈’이라고 한다.
  • 가장 먼저 해야 할 작업은 액션 타입을 정의하는 것이다.
  • 액션 타입은 대문자로 정의하고, 문자열 내용은 모듈 이름/액션 이름과 같은 형태로 작성한다.
  • 문자열 안에 모듈 이름을 넣어줌으로써 나중에 프로젝트가 커졌을 때 액션의 이름이 충돌되지 않게 해준다.
// src/modules/counter.ts

const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
// src/modules/todos.ts

const CHANGE_INPUT = "todos/CHANGE_INPUT"; // input값을 변경함
const INSERT = "todos/INSERT"; // 새로운 todo를 등록
const TOGGLE = "todos/TOGGLE"; // todo를 체크/체크해제 함
const REMOVE = "todos/REMOVE"; // todo를 제거

액션 생성 함수 만들기

  • 액션 타입을 정의한 다음에는 액션 생성 함수를 만들어주어야 한다.
// src/modules/counter.ts

const INSERT = "todos/INSERT"; // 새로운 todo를 등록
const TOGGLE = "todos/TOGGLE"; // todo를 체크/체크해제 함
const REMOVE = "todos/REMOVE"; // todo를 제거
// src/modules/todos.ts

export const insert = (text: string) => ({
  type: INSERT,
  payload: text,
});

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

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

초기 상태 및 리듀서 함수 만들기

  • action type과 state type을 정의해서 리듀서 함수의 파라미터의 action과 initailState의 타입으로 각각 지정해준다.
// src/modules/counter.ts

(...)

type CounterAction = ReturnType<typeof increase> | ReturnType<typeof decrease>;

type CounterState = {
  count: number;
};

const initailState: CounterState = {
  count: 0,
};

const counter = (state = initailState, action: CounterAction) => {
  switch (action.type) {
    case INCREASE:
      return {
        count: state.count + 1,
      };
    case DECREASE:
      return {
        count: state.count - 1,
      };
    default:
      return state;
  }
};

export default counter;
  • Todo 객체의 타입을 따로 정의해준다.
// src/types/Todo.ts

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

export default Todo;
// src/modules/todos.ts
import Todo from "../types/Todo";

(...)

type TodosAction =
  | ReturnType<typeof insert>
  | ReturnType<typeof toggle>
  | ReturnType<typeof remove>;

type TodosState = Todo[];

const initailState: TodosState = [];

const todos = (state = initailState, action: TodosAction): TodosState => {
  switch (action.type) {
    case INSERT:
      const nextId = Math.max(...state.map((todo) => todo.id)) + 1;
      return state.concat({
        id: nextId,
        text: action.payload.toString(),
        done: false,
      });
    case TOGGLE:
      return state.map((todo) =>
        todo.id === action.payload ? { ...todo, done: !todo.done } : todo
      );
    case REMOVE:
      return state.filter((todo) => todo.id !== action.payload);
    default:
      return state;
  }
};

export default todos;

루트 리듀서 만들기

  • createStore 함수를 사용하여 스토어를 만들 때는 리듀서를 하나만 사용해야하기 때문에 기존에 만든 리듀서를 하나로 합쳐주어야 한다.
  • 이 작업은 리덕스에서 제공하는 combineReducers 라는 유틸 함수를 사용하여 쉽게 처리할 수 있다.
  • 모듈이 추가될 때마다 rootReducer 안에 추가해주어야 한다.
// src/modules/index.ts

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

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

export type RootState = ReturnType<typeof rootReducer>;

export default rootReducer;

리액트 애플리케이션에 리덕스 적용하기

  • 스토어를 만들고 리액트 애플리케이션에 리덕스를 적용하는 작업은 src 디렉터리의 index.tsx에서 이루어진다.

스토어 만들기

// index.tsx

(...)

import { legacy_createStore as createStore } from "redux";
import rootReducer from "./modules";

const store = createStore(rootReducer);

Provider 컴포넌트를 사용하여 프로젝트에 리덕스 적용하기

  • App 컴포넌트를 react-redux에서 제공하는 Provider 컴포넌트로 감싸준다.
  • 이 컴포넌트를 사용할 때는 store를 props로 전달해 주어야 한다.
// index.tsx

import { Provider } from "react-redux";

(...)

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

컨테이너 컴포넌트 만들기

  • 컴포넌트에서 리덕스 스토어에 접근하여 원하는 상태를 받아오고, 액션도 디스패치해 줄 차례이다.
  • 리덕스 스토어와 연동된 컴포넌트를 컨테이너 컴포넌트라고 부른다.

CounterContainer 만들기

  • src/containers 디렉터리를 생성하고 그 안에 CounterContainer 컴포넌트를 만든다.
import Counter from "../components/Counter";

const CounterContainer = () => {
  return <Counter count={0} />;
};

export default CounterContainer;
  • 위 컴포넌트를 리덕스와 연동하려면 react-redux에서 제공하는 connect 함수를 사용해야 하며, 다음과 같이 사용한다.
connect(mapStateProps, mapDispatchToProps)(연동할 컴포넌트)
  • mapStateProps : 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수
  • mapDispatchProps: 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수
  • connect 함수를 호출하고 나면 또 다른 함수를 반환하는데,
    반환된 함수에 컴포넌트를 파라미터로 넣어주면 리덕스와 연동된 컴포넌트가 만들어진다.
  • mapStateToProps와 mapDispatchProps에서 반환하는 객체 내부의 값들은 컴포넌트의 props로 전달된다.
  • mapStateToPropsstate를 파라미터로 받아오며, 이 값은 현재 스토어가 지니고 있는 상태를 가르킨다.
  • mapDispatchToProps의 경우 store의 내장 함수 dispatch를 파라미터로 받아온다.
// src/containers/CounterContainer.tsx 

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

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

const mapStateToProps = (state: RootState) => ({
  count: state.counter.count,
});

const mapDispatchToProps = (dispatch: Dispatch) => ({
  increase: () => {
    dispatch(increase());
  },
  decrease: () => {
    dispatch(decrease());
  },
});

export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
  • 기존 App.tsx에서 CounterCounterContainer로 교체한다.
// src/App.tsx

function App() {
  return (
    <div className="App">
      <CounterContainer />
      <hr />
      <Todos />
    </div>
  );
}
  • +1 버튼과 -1 버튼을 눌렀을 때 아래처럼 제대로 작동하는 것을 확인할 수 있다.

  • 일반적으로는 위 코드처럼 mapStateToProps와 mapDispatchToProps를 미리 선언해놓고 사용하지만, 아래처럼 connect 함수 내부에 익명 함수 형태로 선언할 수도 있다.
export default connect(
  (state: RootState) => ({
    count: state.counter.count,
  }),
  (dispatch: Dispatch) => ({
    increase: () => {
      dispatch(increase());
    },
    decrease: () => {
      dispatch(decrease());
    },
  })
)(CounterContainer);
  • 컴포넌트에서 액션을 디스패치하기 위해 각 액션 함수를 호출하고 dispatch로 감싸는 작업이 조금 번거로울 수 있다.
  • 특시 액션 생성 함수의 개수가 많아지면 더더욱 그럴 것이다.
  • 리덕스에서 제공하는 bindActionCreators유틸 함수를 사용하면 간편하다.
  • 두번째 파라미터를 아예 객체 형태로 넣어주면 connect 함수가 내부적으로 bindActionCreator 작업을 해준다.
export default connect(
  (state: RootState) => ({
    count: state.counter.count,
  }),
  (dispatch: Dispatch) => bindActionCreators({ increase, decrease }, dispatch)
)(CounterContainer);

connect 함수 사용은 위처럼 세 가지 방법으로 작성할 수 있다.

TodosContainer 만들기

  • 아래와 같이 TodosContainer를 정의하고, CounterContainer와 마찬가지로 App.tsx에서 호출해주어야 한다.
import { connect } from "react-redux";
import Todos from "../components/Todos";
import { RootState } from "../modules";
import Todo from "../types/Todo";
import { Dispatch, bindActionCreators } from "redux";
import { insert, remove, toggle } from "../modules/todos";

const TodosContainer = ({
  todos,
  insert,
  toggle,
  remove,
}: {
  todos: Todo[];
  insert: (text: string) => void;
  toggle: (id: number) => void;
  remove: (id: number) => void;
}) => {
  return (
    <Todos
      todos={todos}
      onInsert={insert}
      onToggle={toggle}
      onRemove={remove}
    />
  );
};

export default connect(
  ({ todos }: RootState) => ({
    todos: todos,
  }),
  (dispatch: Dispatch) =>
    bindActionCreators(
      {
        insert,
        toggle,
        remove,
      },
      dispatch
    )
)(TodosContainer);
  • 그리고나서 Todos 컴포넌트에서 받아온 props를 사용하도록 구현한다.
// src/components/Todos.tsx

import { useState } from "react";
import Todo from "../types/Todo";
import TodoItem from "./TodoItem";

const Todos: React.FC<{
  todos: Todo[];
  onInsert: (text: string) => void;
  onToggle: (id: number) => void;
  onRemove: (id: number) => void;
}> = ({ todos, onInsert, onToggle, onRemove }) => {
  const [value, setValue] = useState("");

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

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input value={value} onChange={(e) => setValue(e.target.value)} />
        <button type="submit">등록</button>
      </form>
      {todos?.length === 0 ? (
        <div>목록이 비어있습니다.</div>
      ) : (
        <div>
          {todos?.map((todo) => (
            <TodoItem
              todo={todo}
              key={todo.id}
              onToggle={onToggle}
              onRemove={onRemove}
            />
          ))}
        </div>
      )}
    </div>
  );
};

export default Todos;

리덕스 더 편하게 사용하기

  • 타입스크립트에서 리덕스를 사용할 때 typesafe-actions를 활용하면 리덕스를 훨씬 더 편하게 사용할 수 있다.

typesafe-actions

  • 액션 생성 함수를 더 짧은 코드로 작성할 수 있다.
  • 그리고 리듀서를 작성할 때도 switch-case 문이 아닌 createReducer라는 함수를 사용하여 각 액션마다 업데이트 함수를 설정하는 형식으로 작성해 줄 수 있다.

라이브러리 설치

npm install typesafe-actions

createStandardAction 함수 사용

  • createStandardAction을 사용하면 매번 객체를 직접 만들어 줄 필요 없이 더욱 간단하게 액션 생성 함수를 선언할 수 있다.
// src/modulex/couner.ts

// 기존코드
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

// createStandardAction 함수 적용

import { ActionType, createReducer, deprecated } from "typesafe-actions";
const { createStandardAction } = deprecated;

(...)

export const increase = createStandardAction(INCREASE)();
export const decrease = createStandardAction(DECREASE)();
// src/modules/todos.ts

// 기존코드 

export const insert = (text: string) => ({
  type: INSERT,
  payload: text,
});

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

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

// createStandardAction 함수 적용

export const insert = createStandardAction(INSERT)<string>();
export const toggle = createStandardAction(TOGGLE)<number>();
export const remove = createStandardAction(REMOVE)<number>();

ActionType 타입 사용

  • ActionType을 사용하면 액션들의 객체 타입을 만드는 작업을 더욱 짧은 코드로 작성 할 수 있다.
// 기존 코드
type CounterAction = ReturnType<typeof increase> | ReturnType<typeof decrease>;

// ActionType 사용
const actions = { increase, decrease };
type CounterAction = ActionType<typeof actions>;
// 기존 코드

type TodosAction =
  | ReturnType<typeof insert>
  | ReturnType<typeof toggle>
  | ReturnType<typeof remove>;

// ActionType 사용
const actions = {insert,toggle,remove};

type TodosAction =ActionType<typeof actions>

createReducer 함수 사용

  • createReducer를 사용하면 리듀서를 switch/case 문이 아닌 object map 형태로 구현 할 수 있어서 코드가 더욱 간결해지고, 타입 지원도 잘 이루어진다.
// src/modulex/counter.ts

// 기존 코드
const counter = (state = initailState, action: CounterAction) => {
  switch (action.type) {
    case INCREASE:
      return {
        count: state.count + 1,
      };
    case DECREASE:
      return {
        count: state.count - 1,
      };
    default:
      return state;
  }
};

// createReducer 사용 
const counter = createReducer<CounterState, CounterAction>(initailState, {
  [INCREASE]: (state) => ({ count: state.count + 1 }),
  [DECREASE]: (state) => ({ count: state.count - 1 }),
});
// 기존 코드
const todos = (state = initailState, action: TodosAction): TodosState => {
  switch (action.type) {
    case INSERT:
      const nextId =
        state.length > 0 ? Math.max(...state.map((todo) => todo.id)) + 1 : 1;
      return state.concat({
        id: nextId,
        text: action.payload.toString(),
        done: false,
      });
    case TOGGLE:
      return state.map((todo) =>
        todo.id === action.payload ? { ...todo, done: !todo.done } : todo
      );
    case REMOVE:
      return state.filter((todo) => todo.id !== action.payload);
    default:
      return state;
  }
};

// createReducer 사용 
const todos = createReducer<TodosState, TodosAction>(initailState, {
  [INSERT]: (state, { payload: text }) =>
    state.concat({
      id: state.length > 0 ? Math.max(...state.map((todo) => todo.id)) + 1 : 1,
      text,
      done: false,
    }),
  [TOGGLE]: (state, { payload: id }) =>
    state.map((todo) =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ),
  [REMOVE]: (state, { payload: id }) => state.filter((todo) => todo.id !== id),
});

Hooks를 사용하여 컨테이너 컴포넌트 만들기

  • 리덕스 스토어와 연동된 컨테이너 컴포넌트를 만들 때 connect 함수를 사용하는 대신 react-redux에서 제공하는 Hooks를 사용할 수도 있다.

useSelector로 상태 조회하기

  • useSelector Hook을 사용하면 connect 함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있다.
  • useSelector의 사용법은 아래와 같다.
const 결과 = useSelector(상태 선택 함수);
  • 여기서 상태 선택 함수mapStateToProps와 형태가 똑같다.
  • useDispatch는 컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해준다.
  • 컨테이너 컴포넌트에서 액션을 디스패치해야한다면 이 Hook을 사용해야하며, 사용방법은 아래와 같다.
const dispatch = useDispatch()
dispatch({type : "SAMPLE_ACTION:"});

useSelector, useDispatch 사용하기

// src/containers/CounterContainer.tsx

// 기존코드 
const CounterContainer = ({
  count,
  increase,
  decrease,
}: {
  count: number;
  increase: () => void;
  decrease: () => void;
}) => {
  return <Counter count={count} onIncrease={increase} onDecrease={decrease} />;
};

export default connect(
  (state: RootState) => ({
    count: state.counter.count,
  }),
  (dispatch: Dispatch) => bindActionCreators({ increase, decrease }, dispatch)
)(CounterContainer);

// useSelector, useDispatch 사용
const CounterContainer = () => {
  const count = useSelector((state: RootState) => state.counter.count);
  const dispatch = useDispatch();

  return (
    <Counter
      count={count}
      onIncrease={() => dispatch(increase())}
      onDecrease={() => dispatch(decrease())}
    />
  );
};
  • 위와 같이 적용하면 숫자가 바뀌어서 리렌더링될 때마다 onIncrease 함수와 onDecrease 함수가 새롭게 만들어진다.
  • 만약 컴포넌트 성능을 최적화해야 하는 상황이 오면 useCallback으로 액션을 디스패치하는 함수를 감싸주는 것이 좋다.
const CounterContainer = () => {
  const count = useSelector((state: RootState) => state.counter.count);
  const dispatch = useDispatch();
  const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
  const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);

  return (
    <Counter count={count} onIncrease={onIncrease} onDecrease={onDecrease} />
  );
};
// src/containers/TodosContainer.tsx

// 기존 코드
const TodosContainer = ({
  todos,
  insert,
  toggle,
  remove,
}: {
  todos: Todo[];
  insert: (text: string) => void;
  toggle: (id: number) => void;
  remove: (id: number) => void;
}) => {
  return (
    <Todos
      todos={todos}
      onInsert={insert}
      onToggle={toggle}
      onRemove={remove}
    />
  );
};

export default connect(
  ({ todos }: RootState) => ({
    todos: todos,
  }),
  (dispatch: Dispatch) =>
    bindActionCreators(
      {
        insert,
        toggle,
        remove,
      },
      dispatch
    )
)(TodosContainer);

// useSelector, useDispatch,useCallback 사용
const TodosContainer = () => {
  const todos = useSelector((state: RootState) => state.todos);
  const dispatch = useDispatch();

  const onInsert = useCallback(
    (text: string) => dispatch(insert(text)),
    [dispatch]
  );
  const onToggle = useCallback(
    (id: number) => dispatch(toggle(id)),
    [dispatch]
  );
  const onRemove = useCallback(
    (id: number) => dispatch(remove(id)),
    [dispatch]
  );

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

connect 함수 vs. useSelector&useDispatch

  • connect 함수를 사용하여 컨테이너 컴포넌트를 만들었을 경우, 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화된다.
  • 반면 useSelecotr를 사용하여 리덕스 상태를 조회했을 때는 이 최적화 작업이 자동으로 이루어지지 않으므로, 성능 최적화를 위해서는 React.memo를 컨테이너 컴포넌트에 사용해주어야 한다.
  • 지금과 같이 TodosContainer의 부모 컴포넌트인 App이 리렌더링되는 일이 없는 경우에는 불필요한 성능 최적화이다.
export default React.memo(CounterContainer);
export default React.memo(TodosContainer);

정리

  • 리액트 프로젝트에서 리덕스를 사용하면 업데이트에 관련된 로직을 리액트 컴포넌트에서 완벽하게 분리시킬 수 있으므로 유지 보수성이 높은 코드를 작성해낼 수 있다.
  • 규모가 작은 프로젝트에서는 오히려 리덕스를 적용했을 때 프로젝트의 복잡도가 높아질 수 있지만,
  • 규모가 큰 프로젝트에서는 리덕스를 적용하면 상태를 더 체계적으로 관리할 수 있고, 개발자 경험도 향상시켜준다.

참고자료 | https://velog.io/@velopert/use-typescript-and-redux-like-a-pro#typesafe-actions%EB%A1%9C-%EB%A6%AC%EB%8D%95%EC%8A%A4-%EB%AA%A8%EB%93%88-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%ED%95%98%EA%B8%B0

 

TypeScript 환경에서 Redux를 프로처럼 사용하기

이번에 준비한 튜토리얼에서는 TypeScript 환경에서 Redux를 프로처럼 사용하는 방법을 다뤄보도록 하겠습니다. 왜 제목이 "프로처럼"이냐! 사실은 조금 주관적입니다. 이 튜토리얼에서는 지금까지

velog.io

 

profile

DEVELOP

@JUNGY00N