발표자료
발표 자료(PPT) 다운로드 링크
공용 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>
);
}