발표
4주차 발표
이효석

발표자료

발표 자료(PPT) 다운로드 링크

https://docs.google.com/presentation/d/1NUac4epQuQCVcLtd1R4QnX1Dn0N6A5Rw/edit?usp=sharing&ouid=100598926075562912760&rtpof=true&sd=true (opens in a new tab)

공용 API 코드

  • api.js
const BASE_URL = "http://localhost:4000";
 
export async function getTodo() {
  const url = `${BASE_URL}/fetch`;
  const response = await fetch(url);
  return await response.json();
}
 
export async function addTodo(todo) {
  const response = await fetch(`${BASE_URL}/post/${todo}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
  });
 
  if (!response.ok) {
    throw new Error("업로드 실패");
  }
 
  return await response.json();
}

Redux 로 구현 된 TodoList 어플리케이션

단점

  • 모든 로직이 컴포넌트에 집중
  • 비동기 상태 관리를 위한 불필요한 코드 & 변수
  • 서로 다른 로직 사용 시, 협의 필요 -> 협업의 문제
  • 방대해진 Store 코드로 인한 가독성, 리팩토링의 문제

실제 코드

  • Todo 컴포넌트 코드(Todo.jsx)
import { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
 
// redux-store 액션 함수 가져오기
import {
  errorFetch,
  errorPost,
  requestFetch,
  requestPost,
  successFetch,
  successPost,
} from "./store";
 
// 비동기 통신 api 함수 가져오기
import { getTodo, addTodo } from "./api";
 
export default function Todo() {
  const inputRef = useRef();
 
  // redux 사용을 위한 선언 및 상태값 불러오기
  const dispatch = useDispatch();
  const data = useSelector((state) => state.fetchTodo.data);
  const fetchIsLoading = useSelector((state) => state.fetchTodo.isLoading);
  const fetchError = useSelector((state) => state.fetchTodo.error);
 
  const postIsLoading = useSelector((state) => state.postTodo.isLoading);
  const postError = useSelector((state) => state.postTodo.error);
 
  // todolist 불러오는 함수
  const fetchTodo = async () => {
    try {
      dispatch(requestFetch());
      const todoList = await getTodo();
      dispatch(successFetch(todoList));
    } catch (err) {
      dispatch(errorFetch(err));
    }
  };
 
  // todolist 추가하는 함수
  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      dispatch(requestPost());
      await addTodo(inputRef.current.value);
      dispatch(successPost());
      fetchTodo();
    } catch (err) {
      dispatch(errorPost(err));
    }
  };
 
  // 컴포넌트 마운트 시, todolist 불러오는 useEffect
  useEffect(() => {
    fetchTodo();
  }, [dispatch]);
 
  // redux 의 비동기 상태값을 사용한 에러 핸들링 파트
  if (fetchIsLoading || postIsLoading) return <h1>로딩 중</h1>;
 
  if (fetchError || postError) return <h1>에러 발생</h1>;
 
  if (data === undefined) return <h1>리스트 없음</h1>;
 
  // 렌더링 파트
  return (
    <div>
      <ul>
        {data?.map(({ id, content }) => (
          <li key={id}>{content}</li>
        ))}
      </ul>
      <div>
        <input type="text" ref={inputRef} />
        <button onClick={handleSubmit}>할일 추가</button>
      </div>
    </div>
  );
}
  • Redux store 코드(store.js)
import { createStore } from "redux";
 
// 초기 상태
const initialState = {
  fetchTodo: {
    data: [],
    isLoading: false,
    error: undefined,
  },
  postTodo: {
    isLoading: false,
    error: undefined,
  },
};
 
// 액션 타입 정의
const REQUEST_FETCH = "REQUEST_FETCH";
const SUCCESS_FETCH = "SUCCESS_FETCH";
const ERROR_FETCH = "ERROR_FETCH";
 
const REQUEST_POST = "REQUEST_POST";
const SUCCESS_POST = "SUCCESS_POST";
const ERROR_POST = "ERROR_POST";
 
// 액션 생성자
export const requestFetch = () => ({ type: REQUEST_FETCH });
export const successFetch = (data) => ({
  type: SUCCESS_FETCH,
  payload: data,
});
export const errorFetch = (error) => ({
  type: ERROR_FETCH,
  payload: error,
});
 
export const requestPost = () => ({ type: REQUEST_POST });
export const successPost = () => ({ type: SUCCESS_POST });
export const errorPost = (error) => ({
  type: ERROR_POST,
  payload: error,
});
 
// 리듀서
const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case REQUEST_FETCH:
      return {
        ...state,
        fetchTodo: {
          data: undefined,
          isLoading: true,
          error: undefined,
        },
      };
    case SUCCESS_FETCH:
      return {
        ...state,
        fetchTodo: {
          data: action.payload,
          isLoading: false,
          error: undefined,
        },
      };
    case ERROR_FETCH:
      return {
        ...state,
        fetchTodo: {
          data: undefined,
          isLoading: false,
          error: action.payload,
        },
      };
    case REQUEST_POST:
      return {
        ...state,
        postTodo: {
          isLoading: true,
          error: undefined,
        },
      };
    case SUCCESS_POST:
      return {
        ...state,
        postTodo: {
          isLoading: false,
          error: undefined,
        },
      };
    case ERROR_POST:
      return {
        ...state,
        postTodo: {
          isLoading: false,
          error: action.payload,
        },
      };
    default:
      return state;
  }
};
 
// 스토어 생성
const store = createStore(todoReducer);
 
export default store;

Redux-thunk 를 적용한 코드

개선점

  • thunk 의 적용으로 dispatch 에 함수 자체를 전달이 가능 -> Store 에 선언한 함수를 그대로 전달이 가능
  • API 호출 통신 로직을 컴포넌트에서 분리
  • 타 컴포넌트에서도 Store 의 통신 로직을 불러와서 바로 사용 가능
  • 별도의 상태 관리 로직도 Store 에서 통합 관리

남아있는 단점

  • Store 가 더 방대해짐 -> 낮아진 가독성, 리팩토링의 문제점

코드

  • Todo 컴포넌트 코드(Todo.jsx)
import { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
  errorFetch,
  errorPost,
  fetchTodo,
  postTodo,
  requestFetch,
  requestPost,
  successFetch,
  successPost,
} from "./store";
 
export default function Todo() {
  const inputRef = useRef();
 
  const dispatch = useDispatch();
  const data = useSelector((state) => state.fetchTodo.data);
  const fetchIsLoading = useSelector((state) => state.fetchTodo.isLoading);
  const fetchError = useSelector((state) => state.fetchTodo.error);
 
  const postIsLoading = useSelector((state) => state.postTodo.isLoading);
  const postError = useSelector((state) => state.postTodo.error);
 
  // 통신 로직 자체를 Store 에서 불러서 쓰므로, 컴포넌트 코드 자체가 단순해짐
  useEffect(() => {
    fetchTodo();
  }, [dispatch]);
 
  useEffect(() => {
    dispatch(fetchTodo());
  }, [dispatch]);
 
  const handleSubmit = (e) => {
    dispatch(postTodo(inputRef.current.value));
  };
 
  if (fetchIsLoading || postIsLoading) return <h1>로딩 중</h1>;
 
  if (fetchError || postError) return <h1>에러 발생</h1>;
 
  if (data === undefined) return <h1>리스트 없음</h1>;
 
  return (
    <div>
      <ul>
        {data?.map(({ id, content }) => (
          <li key={id}>{content}</li>
        ))}
      </ul>
      <div>
        <input type="text" ref={inputRef} />
        <button onClick={handleSubmit}>할일 추가</button>
      </div>
    </div>
  );
}
  • Redux Store 코드(store.js)
import { createStore, applyMiddleware } from "redux";
import { thunk } from "redux-thunk"; // redux-thunk 불러오기
import { getTodo, addTodo } from "./api"; // API 호출 함수 불러오기
 
// 초기 상태 정의
const initialState = {
  fetchTodo: {
    data: [],
    isLoading: false,
    error: undefined,
  },
  postTodo: {
    isLoading: false,
    error: undefined,
  },
};
 
// 액션 타입 정의
const REQUEST_FETCH = "REQUEST_FETCH";
const SUCCESS_FETCH = "SUCCESS_FETCH";
const ERROR_FETCH = "ERROR_FETCH";
 
const REQUEST_POST = "REQUEST_POST";
const SUCCESS_POST = "SUCCESS_POST";
const ERROR_POST = "ERROR_POST";
 
// 액션 생성자
export const requestFetch = () => ({ type: REQUEST_FETCH });
export const successFetch = (data) => ({
  type: SUCCESS_FETCH,
  payload: data,
});
export const errorFetch = (error) => ({
  type: ERROR_FETCH,
  payload: error,
});
 
export const requestPost = () => ({ type: REQUEST_POST });
export const successPost = () => ({ type: SUCCESS_POST });
export const errorPost = (error) => ({
  type: ERROR_POST,
  payload: error,
});
 
// 컴포넌트에서 사용하던 통신 로직 파트를 Store 에서 구현
// dispatch 를 통해 함수 자체를 전달하는 구조 (thunk 가 없으면 객체만 전달이 가능)
export const fetchTodo = () => {
  return async (dispatch) => {
    dispatch(requestFetch());
    try {
      const data = await getTodo();
      dispatch(successFetch(data));
    } catch (error) {
      dispatch(errorFetch(error));
    }
  };
};
 
export const postTodo = (content) => {
  return async (dispatch) => {
    dispatch(requestPost());
    try {
      await addTodo(content);
      dispatch(successPost());
      dispatch(fetchTodo());
    } catch (error) {
      dispatch(errorPost(error));
    }
  };
};
 
// 리듀서
const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case REQUEST_FETCH:
      return {
        ...state,
        fetchTodo: {
          data: undefined,
          isLoading: true,
          error: undefined,
        },
      };
    case SUCCESS_FETCH:
      return {
        ...state,
        fetchTodo: {
          data: action.payload,
          isLoading: false,
          error: undefined,
        },
      };
    case ERROR_FETCH:
      return {
        ...state,
        fetchTodo: {
          data: undefined,
          isLoading: false,
          error: action.payload,
        },
      };
    case REQUEST_POST:
      return {
        ...state,
        postTodo: {
          isLoading: true,
          error: undefined,
        },
      };
    case SUCCESS_POST:
      return {
        ...state,
        postTodo: {
          isLoading: false,
          error: undefined,
        },
      };
    case ERROR_POST:
      return {
        ...state,
        postTodo: {
          isLoading: false,
          error: action.payload,
        },
      };
    default:
      return state;
  }
};
 
// 스토어 생성
const store = createStore(todoReducer, applyMiddleware(thunk));
 
export default store;

Redux-query 를 적용한 코드

개선점

  • 컴포넌트 상태 관리와 서버 상태 관리를 분리 가능
  • 통신 파트를 개발자가 직접 구현할 필요 없이 react-query 의 기능으로 사용
  • 안정적인 라이브러리 사용으로 별도의 테스트 필요 없음
  • 서버 상태 관리를 위한 불필요한 코드 및 Redux 의 Boilerplate 코드 대폭 감소
    • 컴포넌트 코드 : 80 줄 -> 60줄
    • 서버 상태 관리를 위한 Redux Store 코드 : 132줄 -> 0줄

코드

  • Todo 컴포넌트 코드(TodoQuery.jsx)
import { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
  errorFetch,
  errorPost,
  fetchTodo,
  postTodo,
  requestFetch,
  requestPost,
  successFetch,
  successPost,
} from "./store";
import { getTodo, addTodo } from "./api";
 
// React Query
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
 
export default function TodoQuery() {
  const inputRef = useRef();
  const queryClient = useQueryClient();
 
  const { data, isLoading, isError } = useQuery({
    queryKey: ["todo"],
    queryFn: () => getTodo(),
  });
 
  const postTodoMutation = useMutation({
    mutationFn: (todo) => {
      addTodo(todo);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["todo"] });
    },
  });
 
  const handleSubmit = (e) => {
    e.preventDefault();
 
    postTodoMutation.mutate(inputRef.current.value, {
      onSuccess: () => alert("Todo 등록 성공"),
      onError: () => alert(" 등록 실패"),
    });
 
    inputRef.current.value = "";
    queryClient.invalidateQueries();
  };
 
  if (isLoading) return <h1>로딩 중</h1>;
 
  if (isError) return <h1>에러 발생</h1>;
 
  if (data === undefined) return <h1>리스트 없음</h1>;
 
  return (
    <div>
      <ul>
        {data?.map(({ id, content }) => (
          <li key={id}>{content}</li>
        ))}
      </ul>
      <div>
        <input type="text" ref={inputRef} />
        <button onClick={handleSubmit}>할일 추가</button>
      </div>
    </div>
  );
}