백엔드에서 API가 주어졌고, 나는 React로 TodoList를 만드는 미니 프로젝트를 진행하였다.
요구사항은 다음과 같다.
- 구현 내용
- Data APIs를 제공해주는 React Router 활용하여 아래의 페이지들을 제작합니다
- 페이지 설명
- /: 홈페이지
- /todos: todoList 정보를 확인할 수 있는 페이지
- 페이지에 존재하는 기능
- /
- todo list 제목 작성
- todo list 페이지로 이동하는 버튼 제작
- /todos
- CRUD 기능을 하는 버튼 4개 제작 (TodoList에 대한 로컬 상태관리)
- Firebase Api 명세서를 참고하여 Firebase 연동 및 기능을 구현합니다
- 뒤로가기 버튼 →/으로 이동합니다 (pop)
- CRUD 기능을 하는 버튼 4개 제작 (TodoList에 대한 로컬 상태관리)
- /
- 페이지 설명
- GET : 데이터 읽기
- curl 'https://[SERVER_URL]/todos.json'
- 성공적인 요청은 200 OK HTTP 상태 코드로 표시됩니다. 응답에는 GET 요청의 경로와 관련된 데이터가 포함됩니다.
- [ { id: 0, text: '할 일 1, done: false } ]
- PUT : 데이터 쓰기
- curl -X PUT -d { id: 1, text: '할 일 2', done: false } \\ 'https://[SERVER_URL]/todos/{todoId}.json'
- “성공적인 요청”은 200 OK HTTP 상태 코드로 표시됩니다. “응답”에는 PUT 요청에 지정된 데이터가 포함됩니다.
- { id: 1, text: '할 일 2', done: false }
- PATCH : 데이터 업데이트
- curl -x PATCH -d { done: True } \\ 'https://[SERVER_URL]/todos/{todoId}.json'
- “성공적인 요청”은 200 OK HTTP 상태 코드로 표시됩니다. “응답”에는 PATCH 요청에 지정된 데이터가 포함됩니다.
- { done: True }
- DELETE 데이터 삭제
- curl -x DELETE 'https://[SERVER_URL]/todos/{todoId}.json'
- 성공적인 DELETE 요청은 JSON null 포함하는 응답과 함께 200 OK HTTP 상태 코드로 표시됩니다.
- 테이블이 삭제되더라도, 데이터 쓰기는 정상적으로 작동됩니다
- Data APIs를 제공해주는 React Router 활용하여 아래의 페이지들을 제작합니다
https://github.com/YoonKeumJae/TodoList
쉽게 끝낼 수 있으리라 생각했지만.. 그것은 오만이었다.
생각보다 더 고려할 것들이 있었고, 생각만큼 잘 되지 않았다.
내가 아직 한참 더 공부해야 한다는 반증이겠지..
프로젝트 진행 순서는 다음과 같다.
- 프로젝트 세팅
- 페이지 구성
- 라우팅 설정
- API 연결
- 스타일 적용
- 리팩토링
1. 프로젝트 세팅
프로젝트 생성: CRA
가장 먼저 CRA로 프로젝트를 생성하였다.
Vite을 쓰는 선택지도 있었지만, 익숙한 CRA로 개발하고 나중에 필요 시 리팩토링 하기로 했다.
라우팅: react-router-dom
스타일: SCSS
Styled-components를 주로 사용했지만, 앞으로 진행하게 될 프로젝트에서 SCSS를 사용할 예정이라 연습삼아 사용했다.
2. 페이지 구성
페이지는 메인 페이지, 리스트 페이지 두 장만 있었다.
메인 페이지에서는 제목을 설정할 수 있고, 리스트 페이지로 이동하는 버튼이 있다.
리스트 페이지에서는 메인 페이지로 돌아가는 버튼이 있고, DB에 저장된 TodoList에 대한 CRUD기능을 사용할 수 있다.
디렉토리 구조는 위와 같다.
pages에는 페이지를 대표하는 컴포넌트가 있다.
components에는 TodoList에서 하나의 항목을 나타내는 Todo, 버튼을 눌렀을 때만 보이는 TodoInput이 있다.
index는 코드 작성의 효율을 위해 디렉토리 내 모든 컴포넌트를 import, 한꺼번에 export하였다.
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import "../styles/pages/Home.scss";
const Home = () => {
const [title, setTitle] = useState("My TodoList");
const [inputValue, setInputValue] = useState("");
const handleTitle = (e) => {
e.preventDefault();
if (inputValue.trim() === "") {
alert("공백은 입력 불가입니다");
document.querySelector(".input__title").value = "";
return;
}
setTitle(inputValue.trim());
document.querySelector(".input__title").value = "";
};
const handleInput = (e) => {
e.preventDefault();
setInputValue(e.target.value);
};
const navigate = useNavigate();
const moveToList = () => {
navigate("/todos");
};
return (
<div className="wrapper">
<h1 className="title">{title}</h1>
<div className="body__wrapper">
<form className="inputForm">
<input
type="text"
placeholder="Input title"
onChange={handleInput}
className="input__title"
/>
<button type="submit" onClick={handleTitle} className="btn__title">
OK
</button>
</form>
<button type="button" onClick={moveToList} className="btn__move">
Move to List
</button>
</div>
</div>
);
};
export default Home;
Home.js의 코드이다.
useState를 사용하여 title을 사용자가 변경할 수 있게 하였다.
useNavigate를 사용하여 버튼을 누를 시 '/todos'로 이동할 수 있게 하였다.
공백을 입력할 경우, alert로 경고창을 띄워 예외처리를 하였다.
또한 문자열 앞뒤로 공백을 없앴다.
버튼이 눌리면 input 내부는 비어있게 된다.
import { getTodos } from "../api";
import { useState, useEffect } from "react";
import { Todo, TodoInput } from "../components";
import { v4 as uuidv4 } from "uuid";
import { useNavigate } from "react-router-dom";
import "../styles/pages/Todos.scss";
const Todos = () => {
const [todos, setTodos] = useState([]);
const [formVisible, setFormVisible] = useState(false);
useEffect(() => {
const updateData = async () => {
try {
const res = await getTodos();
if (res === null) {
const newId = uuidv4();
return [
...todos,
{ id: newId, text: "아직 아무것도 없어요!", done: false },
];
}
setTodos(res);
} catch (e) {
console.error(e);
}
};
updateData();
}, [todos]);
const viewForm = () => {
setFormVisible(!formVisible);
};
const navigate = useNavigate();
return (
<div className="wrapper">
<h1 className="title">Todos</h1>
<button type="button" onClick={() => navigate("/")}>뒤로가기</button>
<div className="form">
<button type="button" onClick={viewForm} className="btn__newTask">
➕ Click to add a new task!
</button>
{formVisible ? <TodoInput /> : null}
<div className="todoList">
{Object.values(todos).map((todo) => (
<div key={todo.id}>
<Todo todo={todo} />
</div>
))}
</div>
</div>
</div>
);
};
export default Todos;
Todos.js의 코드이다.
useNavigate로 홈 화면으로 돌아가는 버튼을 구현하였다.
또한 useState를 사용하여 새 todo를 입력하는 컴포넌트 TodoInput을 토글 버튼으로 보이게, 또는 보이지 않게 할 수 있다.
객체의 배열인 todos를 map함수로 리스트의 todo 하나를 가리키는 컴포넌트 Todo를 생성하였다.
API 사용과 관련된 코드는 나중에 설명하겠다.
import { putTodos } from "api";
import { useState } from "react";
import { v4 as uuidv4 } from "uuid";
import "../styles/components/TodoInput.scss";
const TodoInput = () => {
const [todo, setTodo] = useState("");
const todoChangeHandler = (e) => {
setTodo(e.target.value);
};
const newId = uuidv4();
const onClickSubmit = (e) => {
if (todo === "") {
alert("공백은 입력 불가입니다");
return;
}
e.preventDefault();
putTodos({
id: newId,
text: todo.trim(),
done: false,
});
document.querySelector(".input__text").value = "";
};
return (
<form className="form">
<input
type="text"
placeholder="I have to do..."
onChange={todoChangeHandler}
className="input__text"
></input>
<button type="submit" onClick={onClickSubmit}>
OK
</button>
</form>
);
};
export default TodoInput;
TodoInput.js의 코드이다.
홈 화면과 마찬가지로 입력 후 버튼을 눌렀을 때의 예외처리를 해주었다.
import { useState } from "react";
import { patchTodos, deleteTodos } from "api";
import "../styles/components/Todo.scss";
const Todo = ({ todo }) => {
const [done, setDone] = useState(todo.done);
const onStatusEdit = (e) => {
e.preventDefault();
patchTodos({
id: todo.id,
text: todo.text,
done: !done,
});
setDone(!done);
};
const onDelete = (e) => {
e.preventDefault();
deleteTodos(todo.id);
};
return (
<div className="wrapper__todo">
<div className="todo">
{done ? (
<span onClick={onStatusEdit}>✅</span>
) : (
<span onClick={onStatusEdit}>❎</span>
)}
<span className="task">{todo.text}</span>
<button type="button" onClick={onDelete} className="btn__delete">
Delete
</button>
</div>
</div>
);
};
export default Todo;
마지막으로 Todo.js이다.
리스트의 하나를 가리키는 컴포넌트 이다.
처음에는 useState로 onStatusEdit을 작성하였지만(const [isDone, setIsDone] = useState(false);) API를 연결하면서 위와 같이 수정하게 되었다.
3. 라우팅 설정
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { Home, Todos } from "./pages";
import "./styles/App.scss"
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/todos",
element: <Todos />,
},
]);
function App() {
return (
<div>
<RouterProvider router={router} />
</div>
);
}
export default App;
App.js에서 react-router-dom의 기능을 사용하였다.
createBrowserRouter로 객체의 배열을 생성하여 path와 element 속성을 줬다.
4. API 연결
API를 연결하는 과정이 제일 험난했다...
먼저 api.js를 만들고 api 관련 함수들을 작성하였다.
import api_addr from "apiKey";
const getTodos = async () => {
const response = await fetch(`${api_addr}todos.json`);
const data = await response.json();
return data;
};
const putTodos = async (todo) => {
const response = await fetch(`${api_addr}todos/${todo.id}.json`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(todo),
});
if (response.ok) {
return response.json();
} else {
console.log("error");
throw new Error("Error");
}
};
const patchTodos = async (todo) => {
const response = await fetch(`${api_addr}todos/${todo.id}.json`, {
method: "PATCH",
body: JSON.stringify({ done: todo.done }),
});
if (response.ok) {
return response.json();
} else {
throw new Error("Error");
}
};
const deleteTodos = async (id) => {
const response = await fetch(`${api_addr}todos/${id}.json`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
if (response.ok) {
return response.json();
} else {
throw new Error("Error");
}
};
export { getTodos, putTodos, patchTodos, deleteTodos };
요청을 보낼 때는 headers에 "나 json파일 보낼거야" 라는 메세지를 보내야한다.
코드가 잘 작동하지 않아 chatGPT에게 물어보고 나서야 헤더를 붙이지 않았다는 것을 깨닫게 되었다..
api 함수를 작성했으니, 기능이 필요한 컴포넌트에 api를 연결할 차례이다.
useEffect(() => {
const updateData = async () => {
try {
const res = await getTodos();
if (res === null) {
const newId = uuidv4();
return [
...todos,
{ id: newId, text: "아직 아무것도 없어요!", done: false },
];
}
setTodos(res);
} catch (e) {
console.error(e);
}
};
updateData();
}, [todos]);
이 코드는 Todos.js의 일부이다.
처음에는 useEffect의 의존성 배열로 빈 배열을 줬다.
이 컴포넌트가 처음 렌더링이 될 때만 이 함수를 실행하고 싶었기 때문이다.
하지만 todos 배열이 수정된 후에 사용자가 일일이 새로고침을 눌러야 리스트가 업데이트 되었다.
이 문제는 useEffect의 의존성 배열에 todos를 추가해주니 해결되었다.
todos가 변경될 때 마다 이 기능을 수행하는 것이다.
이렇게 하면 처음 마운트 될 때는 아무것도 동작하지 않는 줄 알았는데 아니었다.
import { v4 as uuidv4 } from "uuid";
...
const newId = uuidv4();
...
putTodos({
id: newId,
text: todo.trim(),
done: false,
});
TodoInput.js의 일부분이다.
putTodos는 새로운 Todo를 추가하는 함수이다.
각각의 Todo는 중복되지 않는 특정한 id값을 가져야 한다.
처음에는 todos.length+1로 해서 id값을 주려고 했지만, 생각대로 잘 되지 않았다.
한참을 고민하다가 chatGPT에게 물어봤는데, uuid를 사용하면 된다고 했다.
이렇게 해서 id에 대한 문제가 해결되었지만, Todo의 인덱스 처리가 되지 않는다는 문제가 새로 생겼다.
코드를 직접 실행시켜보면 알겠지만, Todo를 추가했을 때 순서대로 추가되는 것이 아니라 자기 멋대로 추가된다.
id값을 기준으로 TodoList에 정렬되는데, 이 id값은 랜덤으로 정해지기 때문이다.
이 문제는 아직 고치지 못했고 추후 수정 예정이다.
const onStatusEdit = (e) => {
e.preventDefault();
patchTodos({
id: todo.id,
text: todo.text,
done: !done,
});
setDone(!done);
};
const onDelete = (e) => {
e.preventDefault();
deleteTodos(todo.id);
};
Todo.js의 일부이다.
Todo에 대한 완료 상태(done)를 바꾸는 버튼을 누르면 patchTodos를 통해 데이터를 수정한다.
삭제 버튼을 누르면 todo.id를 전달해 데이터를 삭제한다.
..이제 돌아보니 정말 쉬워보이지만 막상 할 때는 정말 골치아팠다.
실수로 id값을 주지 않은 채 put요청을 보내서 이상한 데이터를 가득 채우기도 했다.
get요청으로 받은 배열을 순회하며 delete해서 문제를 해결할 수 있었다.
5. 스타일 적용
평소에는 styled-components를 사용하여 스타일을 작성하였지만, 다음에 진행 예정인 프로젝트에서 SASS를 사용할 예정이라 연습삼아 사용해보았다.
잘 했다고 생각한 것이, SASS에서는 클래스의 이름이 중복돼서는 안됐다.
styled-components에서는 컴포넌트 마다 wrapper, body 등의 클래스 이름을 썼지만 SASS에서는 중복된 클래스 이름을 사용하지 않기 위해 머리를 써야했다.
6. 리팩토링
홈 페이지의 input 태그는 예외처리가 되어있지 않는다거나, api 링크를 숨겨놓지 않았다던가 하는 등의 사항들을 수정하였다.
이후로 Todo의 index 관련 문제, 최적화 문제 등을 수정할 예정이다.
'FE > React' 카테고리의 다른 글
[React Official Document] #2 Thinking in React (0) | 2023.09.26 |
---|---|
[React Official Document] #1 Quick Start (0) | 2023.09.25 |
6. 외부 API 연동하여 뉴스 뷰어 만들기 (0) | 2023.08.01 |
5. React router로 SPA(Single Page Application) 개발하기 (0) | 2023.08.01 |
4. immer를 사용하여 더 쉽게 불변성 유지하기 (0) | 2023.07.28 |