전체 코드는 다음 레포지터리에서 구경할 수 있습니다.
https://github.com/BrightJun96/wanted-pre-onboarding-challenge-fe-27
종택님께서 진행하시는 11월 원티드 프리온보딩 사전과제(TO DO APP)를 진행했어요. 주어진 API를 통한 기본적인 TO DO APP을 구현하는데요. CRUD(등록,수정,삭제,상세조회,목록조회) 정도의 기능이 있어요.
사전과제의 요구사항 중 하나로 다음과 같은 사항이 있었습니다.
window.location.reload
없이 목록을 업데이트해주세요.요구사항을 반영하기전에는 등록,수정,삭제시에 대한 코드가 대략 다음과 같았어요.
async function networkAddTodo() {
// 등록 API
const response = await fetchCreateTodo(todoForm)
if(response.ok){
window.alert("할일이 등록되었습니다.")
// 목록 API
await fetchGetTodos()
/**
* @TODO 새로고침없이 데이터 갱신
*/
window.location.reload()
}else{
window.alert("할일 등록에 실패했습니다.")
}
}
등록 API가 성공한다면 목록 API를 호출해주고 window.location.reload()
를 통해 목록을 갱신해줬죠.
수정,삭제 또한 위 코드처럼 진행해줬습니다. 하지만 요구사항대로 하는 것이 UX적으로 더 낫다고 생각하는데요. window.location.reload()
을 하게 되면 화면을 리로드하기 때문에 화면이 깜빡이게 됩니다. 이는 UX에 좋지 않죠.
어쨋든 window.location.reload()
를 하지 않고 목록을 갱신해줘야합니다. 어떻게 해야할까요?
async function networkAddTodo() {
// 등록 API
const response = await fetchCreateTodo(todoForm)
if(response.ok){
window.alert("할일이 등록되었습니다.")
// 목록 API
await fetchGetTodos()
}else{
window.alert("할일 등록에 실패했습니다.")
}
}
위 코드에서도 등록 API 호출을 성공하면 목록 API를 호출해주고 있어요. window.location.reload
코드를 제거하면 등록 API가 성공해도 목록이 갱신이 안되는데요.
window.location.reload
를 해주지 않으면 목록이 왜 갱신이 안될까요?
다음 코드를 보시죠. 할일 목록 API와 컴포넌트에 대한 코드입니다.
// 할일 목록 조회 API
export async function fetchGetTodos(){
const response = await networkInstance(`${TODOS}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
auth: true,
})
if(response.ok){
return await response.json()
}else {
window.alert("할일 목록을 불러오는데 실패했습니다.")
}
}
할일 목록 컴포넌트
import TodoItem from "./todoItem.tsx";
import TodoAddButton from "./todoAddButton.tsx";
import {useEffect, useState} from "react";
import {fetchGetTodos} from "../../../../service/todos/api.todos.ts";
import {useEffect} from "react";
import {TodoListItemResponse} from "../../../../service/todos/types.ts";
import {useNavigate} from "react-router-dom";
function TodoList() {
const [todoList, setTodoList] = useState<TodoListItemResponse[]>([]);
const navigate = useNavigate()
useEffect(() => {
async function networkFetchGetTodos() {
const response = await fetchGetTodos();
const data = await response.json()
if(response.ok){
setTodoList(data.data)
if(data.data.length>0) {
navigate(`/todo/${data.data[0].id}`)
}
}else {
window.alert("할일 목록을 불러오는데 실패했습니다.")
}
}
networkFetchGetTodos();
}, []);
return (
<div className={"todo-list-container"}>
<TodoAddButton/>
{todoList.map((todoItem) =>
<TodoItem key={todoItem.id} todoItem={todoItem}
/>)}
</div>
);
}
export default TodoList;
할일 목록 컴포넌트에서 API를 호출하고 데이터를 담는 server State 갱신을 하고 있어요.
그렇다면 등록,수정,삭제 API가 호출할 때에 server State를 갱신해주면 되겠네요. 하지만 등록,수정,삭제 API가 호출은 할일 상세 컴포넌트에서 호출되는데요.
import {createBrowserRouter} from "react-router-dom";
import {AuthCheck} from "../helper/auth/authCheck.ts";
import Login from "../page/login/login.tsx";
import Root from "../page/root/root.tsx";
import Signup from "../page/signup/signup.tsx";
import TodoDetails from "../page/todo/details/todoDetails.tsx";
import TodoRegister from "../page/todo/register/todoRegister.tsx";
import Todo from "../page/todo/todo.tsx";
const router = createBrowserRouter([
{
path:"/",
element:<Root/>,
children:[
{
path: "todo",
element:<Todo/>, // 할일 목록 컴포넌트
loader: AuthCheck.authPageCheck,
children:[
{
path: ":id",
element:<TodoDetails/> // 할일 상세 페이지 컴포넌트
},
{
path: "register",
element:<TodoRegister/>
}
]
},
{
path:"auth",
loader: AuthCheck.notAuthPageCheck,
children:[
{
path:"login",
element:<Login/>,
},
{
path:"signup",
element:<Signup/>
}
]
}
]
}
]);
export default router;
server State의 부모 컴포넌트에서 선언하여 props로 전달하려하여도 라우트자체가 달라 전달할 수가 없습니다.
그래서 이럴 때 필요한 Context API를 사용하기로 했죠
context에 server State와 setState를 전달하기보다는 목록 갱신하는 함수 자체를 context로 지정해봤어요.
server State와 setState를 context로 지정하면 아래와 같은 로직이 등록,수정,삭제 이후 어차피 똑같은 코드가 반복될테니 차라리 갱신하는 역할의 함수 자체를 전달하면 반복 코드가 줄기 때문이죠.
// 목록 갱신 함수
async function networkFetchGetTodos() {
const data = await fetchGetTodos();
setTodoList(data.data)
if(data.data.length>0) {
navigate(`/todo/${data.data[0].id}`)
}
}
목록 갱신 Context
import {createContext} from "react";
// 할일 목록 네트워크 업데이트 컨텍스트
export const TodoListNetworkUpdateContext = createContext(async()=>{})
할일 목록 컴포넌트
import TodoList from "../../components/feature/todo/todoList/todoList.tsx";
import "../../css/index.css"
import "../../css/todo/todo.css"
import {FLEX_ROW_CONTAINER_CLASSNAME} from "../../constant/css/constant.ts";
import {Outlet} from "react-router-dom";
import {Outlet, useNavigate} from "react-router-dom";
import {TodoListNetworkUpdateContext} from "../../context/todo/todoContext.ts";
import {fetchGetTodos} from "../../service/todos/api.todos.ts";
import {useState} from "react";
import {TodoListItemResponse} from "../../service/todos/types.ts";
function Todo() {
const [todoList, setTodoList] = useState<TodoListItemResponse[]>([]);
const navigate = useNavigate()
async function networkFetchGetTodos() {
const data = await fetchGetTodos();
setTodoList(data.data)
if(data.data.length>0) {
navigate(`/todo/${data.data[0].id}`)
}
}
return (
<TodoListNetworkUpdateContext.Provider value={networkFetchGetTodos}>
<div
className={`${FLEX_ROW_CONTAINER_CLASSNAME} todo-page-container`}
>
{/*할일 목록*/}
<TodoList
todoList={todoList}
networkFetchGetTodos={networkFetchGetTodos}
/>
<Outlet/>
</div>
</TodoListNetworkUpdateContext.Provider>
);
}
할일 목록 갱신 함수인 networkFetchGetTodos
함수를 Context Value로 지정하였습니다.
Outlet
에는 등록,수정,삭제 API를 호출하는 상세 컴포넌트가 있죠. 상세 컴포넌트에서는 context에 담긴 networkFetchGetTodos
함수를 불러와서 수정,삭제,등록 API가 호출된 뒤에 호출해주죠.
코드는 다음과 같습니다.
상세 컴포넌트
import React, {useContext, useEffect, useState} from 'react';
import CustomInput from "../../../input/customInput.tsx";
import CustomButton from "../../../button/customButton.tsx";
import {
fetchCreateTodo, fetchDeleteTodo,
fetchGetTodoById,
fetchGetTodos,
fetchUpdateTodo
} from "../../../../service/todos/api.todos.ts";
import {TODO_PAGE_ENUM, TODO_PAGE_TYPE} from "../../../../constant/feature/todo/constant.ts";
import {useParams} from "react-router-dom";
import {TodoListNetworkUpdateContext} from "../../../../context/todo/todoContext.ts";
interface TodoFormInterface {
title: string;
@@ -30,6 +30,7 @@
})
const networkFetchGetTodos= useContext(TodoListNetworkUpdateContext)
const params = useParams()
@@ -42,117 +43,109 @@
async function networkAddTodo() {
const response = await fetchCreateTodo(todoForm)
if(response.ok){
// 할일 목록 갱신
await networkFetchGetTodos()
}
}
// 할일 수정 네트워크 요청
async function networkUpdateTodo(detailsId: string) {
const response = await fetchUpdateTodo(detailsId, todoForm)
i
// 할일 목록 갱신
await networkFetchGetTodos()
}
}
// 할일 삭제 네트워크 요청
async function networkDeleteTodo(detailsId: string|undefined) {
if(detailsId) {
const response = await fetchDeleteTodo(detailsId)
if(response.ok) {
// 할일 목록 갱신
await networkFetchGetTodos()
}
}
else{
window.alert("상세 ID가 없습니다.")
}
}
// 폼 변경
function handleFormChange( key: keyof TodoFormInterface,value: string) {
setTodoForm({
...todoForm,
[key]: value
})
}
// 폼 제출
async function handleFormSubmit(
event: React.FormEvent<HTMLFormElement>
) {
event.preventDefault();
// 등록, 수정 분기 처리
if(pageType === IsDetailsPage && params.id){
await networkUpdateTodo(params.id)
}else{
await networkAddTodo();
}
}
// 버튼 비활성화 조건
const ButtonDisabledCondition = todoForm.title === "" || todoForm.content === "";
// 버튼 라벨
const ButtonLabel = pageType===IsDetailsPage? "수정":"등록"
useEffect(() => {
async function networkFetchGetTodo(detailsId: string) {
const data = await fetchGetTodoById(detailsId)
setTodoForm({
title: data.data.title,
content: data.data.content
})
}
if((pageType===IsDetailsPage)&¶ms.id){
networkFetchGetTodo(params.id)
}
}, [params.id]);
return (
<div className={"todo-details-container"}>
<form
className={"todo-details-form-container"}
onSubmit={handleFormSubmit}
>
{/*제목*/}
<CustomInput
label={"제목"}
value={todoForm.title}
onChange={(value) => handleFormChange("title",value)}
inputType={"text"}/>
{/*내용 */}
<CustomInput
label={"내용"}
value={todoForm.content}
onChange={(value) => handleFormChange("content",value)}
inputType={"text"}/>
{/*등록,수정*/}
<CustomButton
type={"submit"}
disabled={ButtonDisabledCondition}
label={ButtonLabel} />
{/*삭제*/}
<CustomButton
label={"삭제"}
onClick={() => networkDeleteTodo(params.id)}/>
</form>
</div>
);
}
export default TodoDetailsForm;
networkFetchGetTodos
함수에는 할일목록 API 호출뿐만 아니라 server State도 갱신하는 코드가 있기 때문에 목록을 갱신할 수 있습니다.
서로 다른 라우트에 있는 컴포넌트가 있고 A컴포넌트의 server State를 B 컴포넌트에서 변경해줘야할 때, 결국에 전역적으로 값을 사용하기 위해 Context API를 사용하여 해결해보았습니다.
전체 코드는 다음 레포지터리에서 구경할 수 있습니다.
https://github.com/BrightJun96/wanted-pre-onboarding-challenge-fe-27