어찌 Context API에 대한 글이 없는 것인가? ㅠㅠ 나의 실수당.
왜 없는 것인가? 생각에 생각을 도돌이표 해보닝
https://react.dev/reference/react/useContext
에 나름 설명이 아주 잘 되어 있어서 그랬던 것 같다. 하지만 부족한 부분이 있다.
더미 데이터를 이용했기 때문에 비동기 초기화 및 CRUD 부분이 빠져 있당.
JSON-SERVER 를 이용해서 간단히 B/E를 구성하여
핵심 중요 예제(5번)인 todo 리스트 예제를 재 구성하여 보장.
단순히 더미데이터를 가지고 하는 것과 실제 B/E 연동은
실행 환경 관점에서 보면 꽤나 달라 보일 수도 있당. (달라 미국 달라 달라!)
먼저 Context API를 쓰면 좋은 점은 Props Drilling은 컴포넌트 구조가 복잡해지고
단계가 깊어지면 사용이 어려워 지는 단점이 있는데 (그렇당),
Context API를 쓰면 필요한 곳에서 바로 꺼내 쓸 수 있는 장점이 있당. (그렇당)
억지 오버로 현실에 비유하면 Props Drilling은 위에서 아래로 빠짐없이 절차를
지켜야 하지만 Context API는 "어명이옹 어서 그것을 내어 주시옹!" 하는 것고 같을지어당.
말이 길어지면 안 그래도 졸린데, 더욱 더 졸리게 될 꺼이당. 그냥 해보장
먼저 B/E 구성을 위해 todos-server란 이름으로 디레토리를 1개 맹글장.
그라공 파일 데이터베이스로 쓸 km-todos.json 파일을 아래 처럼 맹글장.
km-todos.json
{
"todos": [
{ "id": 1, "text": "기획자로 거듭나깅", "done": true },
{ "id": 2, "text": "위대한 기획자 되깅", "done": false },
{ "id": 3, "text": "술 적당히 자제하깅", "done": false },
{ "id": 4, "text": "개발도 잘하는 기획자", "done": false}
]
}
cmd 터미널 창을 열어 아래 명령으로 json-server를 구동하장.
npx json-server km-todos.json -p 8272
이제 브라우져 주소 표시줄에 http://localhost:8272/todos 를 입력하면 리스트가 보여야 한당.
boomerang 같은 rest client 프로그램으로 테스트 해보면, post,put, delete 다 동작한당.
B/E 준비 끄읕!
이제 F/E (Front-End)를 준비해 보장.
km-context란(km은 경민이당) 폴더를 만들고, 이것을 프로젝트 폴더로 하장.
[ tailwindcss, axios, lucide-react 설치 및 필요없는 파일 정리는 알아서 스스로 하장 ]
먼저 Context API를 사용하기 위해 Context를 생성하는 아래 파일을 만들장
[ 일부러 분리 하였고, useReducer 훅에 쓸 tasksReducer함수도 일부러 여기엥 ]
tasksContext.js
import { createContext } from "react";
// context 생성 & export
export const TasksContext = createContext();
// useReducer에 쓸 reducer
export const tasksReducer = (tasks, action) => {
switch (action.type) {
case "getList": {
return action.payload;
}
default:
throw new Error("그런 케이스는 없어용");
}
};
ContextProvider는 분리하는 것이 사용에 유리함이 있당.
ContextProvider.jsx
import { useEffect, useReducer } from "react";
import { TasksContext, tasksReducer } from "./tasksContext";
import axios from "axios";
function ContextProvider({ children }) {
// 처음엔 데이터가 없으닝, 빈 배열로 초기화
const [tasks, tasksDispatch] = useReducer(tasksReducer, []);
useEffect(() => {
axios.get("http://localhost:8272/todos").then((resp) => {
tasksDispatch({ type: "getList", payload: resp.data });
});
}, []);
return (
// useContext 바로 tasks와 tasksDispatch를 꺼내 쓸 수 있도록 value에 넘김
<TasksContext.Provider value={{ tasks, tasksDispatch }}>
{children}
</TasksContext.Provider>
);
}
export default ContextProvider;
ContextProvider를 App에 적용하장
App.jsx
import ContextProvider from "../ContextProvider";
import AddTask from "./AddTask";
import TaskList from "./TaskList";
function App() {
return (
<>
<ContextProvider>
<div className="flex flex-col justify-center items-center mt-5">
<h1 className="text-5xl text-violet-600 font-extrabold bg-amber-300 rounded-2xl px-5 mb-5">
<img
className="inline-block"
width={100}
height={100}
src="https://api.dicebear.com/9.x/adventurer/svg?seed=Jack"
alt="lkm"
/>{" "}
리경민 기획자 ToDo
</h1>
<AddTask />
<TaskList />
</div>
</ContextProvider>
</>
);
}
export default App;
Todo를 등록하는 기능을 가진 컴포넌트 AddTask
AddTask.jsx
import { useContext, useState } from "react";
import { TasksContext } from "../tasksContext";
import axios from "axios";
function AddTask() {
const [text, setText] = useState("");
const { tasksDispatch } = useContext(TasksContext);
const chgTxt = (e) => {
setText(e.target.value);
};
const btnClick = () => {
setText("");
// id 관리는 괘니 localStorage를 이용
let maxTodoId = 5;
if (!localStorage.getItem("maxTodoId")) {
localStorage.setItem("maxTodoId", maxTodoId);
} else {
maxTodoId = parseInt(localStorage.getItem("maxTodoId")) + 1;
localStorage.setItem("maxTodoId", maxTodoId + 1);
}
const newTask = {
id: maxTodoId + "",
text: text,
done: false,
};
axios.post("http://localhost:8272/todos", newTask).then(() => {
axios.get(`http://localhost:8272/todos`).then((resp) => {
tasksDispatch({
type: "getList",
payload: resp.data,
});
});
});
};
return (
<div className="text-4xl mt-3 mb-3">
<input
className="border-2 border-blue-600 rounded-2xl mr-5"
placeholder="할 일 추가하삼"
autoFocus
value={text}
onChange={chgTxt}
/>
<button
className="rounded-2xl bg-slate-800 text-white p-2"
onClick={btnClick}
>
추가
</button>
</div>
);
}
export default AddTask;
Todo 리스트를 뿌리는 TaskList
TaskList.jsx
import { useContext } from "react";
import { TasksContext } from "../tasksContext";
import Task from "./Task";
function TaskList() {
const { tasks } = useContext(TasksContext);
return (
<ul>
{tasks.length ? (
tasks.sort((a,b)=> b.id - a.id).map((task) => (
<li key={task.id}>
<Task task={task} />
</li>
))
) : (
<h1>등록된 할 일이 없어용 </h1>
)}
</ul>
);
}
export default TaskList;
Todo 컴포넌트
Task.jsx
import { useContext, useEffect, useRef, useState } from "react";
import { TasksContext } from "../tasksContext";
import { Delete, Pencil, Save } from "lucide-react";
import axios from "axios";
function Task({ task }) {
const [ isEditing, setIsEditing ] = useState(false);
const { tasksDispatch } = useContext(TasksContext);
const txtRef = useRef(null);
useEffect(() => {
if(isEditing){
txtRef.current.focus();
}
},[isEditing])
const chgCkbox = (e) => {
const modTask = {
id:task.id,
text:task.text,
done: e.target.checked
}
axios.put(`http://localhost:8272/todos/${task.id}`,modTask).then(() => {
axios.get(`http://localhost:8272/todos`).then(resp =>{
tasksDispatch({
type: "getList",
payload: resp.data
});
})
});
};
const saveClick = (e) =>{
const modTask = {
id:task.id,
text:txtRef.current.value,
done: e.target.checked
}
axios.put(`http://localhost:8272/todos/${task.id}`,modTask).then(() => {
axios.get(`http://localhost:8272/todos`).then(resp =>{
tasksDispatch({
type: "getList",
payload: resp.data
});
})
});
setIsEditing(false);
}
const deleteClick = () => {
axios.delete(`http://localhost:8272/todos/${task.id}`).then(() => {
axios.get(`http://localhost:8272/todos`).then(resp =>{
tasksDispatch({
type: "getList",
payload: resp.data
});
})
});
};
let taskContent;
if (isEditing) {
taskContent = (
<div className="inline-flex m-3 w-100 justify-around">
<input className="w-[75%] border-2 border-pink-500 rounded-2xl" ref={txtRef} defaultValue={task.text} />
<button className="bg-amber-800 text-white rounded-2xl px-5" onClick={ saveClick }>
<Save size={32} />
</button>
</div>
);
} else {
taskContent = (
<div className={`inline-flex m-3 w-100 justify-around ${task.done?"line-through":"" } `}>
<span className="w-[75%]">{task.text}</span>
<button
className="bg-blue-600 text-white rounded-2xl px-5"
onClick={() => setIsEditing(true)}
>
<Pencil color={"yellow"} size={32} />
</button>
</div>
);
}
return (
<div className="text-3xl">
<input
className="scale-200"
type="checkbox"
checked={task.done}
onChange={chgCkbox}
/>
{taskContent}
<button
className="bg-red-600 text-white rounded-2xl px-5 relative top-1"
onClick={deleteClick}
>
<Delete size={34} />
</button>
</div>
);
}
export default Task;
실행 결과
결과가 잘 나온다면, Custom Hook, Custom function을 만들어서
Refactoring 연습을 해 볼 절호의 찬스당. 자 맘을 가라 앉히고 해보장.
분명 손으로 타이핑한 그들에겐 오타가 있을지도 모른당.
내가 쓴 위 소스에도 어쩌면 있을 지 모른당. 역시 자신 할 수 없당.
그래서 전체 소스를 괘니 첨부해 본당. 연습만이라도 해보도록 하장.
좋아하게 된 뒤에 (After LIKE) 무슨 일이...
아마도 React 공부에 매진!!!
매진 뒤에 깨닫는다. 대략 만들고픈 컴포넌트들이 거의 이미
만들어져 세상에 출렁 출렁 파도치고 있음을...
https://www.youtube.com/watch?v=F0B7HDiY-10
React(리액트) 티키타카 35 (useReducer Hook(훅)) (2) | 2025.05.07 |
---|---|
React(리액트) 티키타카 34 (@tanstack/react-query) (0) | 2025.04.08 |
React(리액트) 티키타카 33 (wx-react-gantt) SVAR Gantt (3) | 2025.03.27 |
React(리액트) 티키타카 32 (motion) 애니메이션 (0) | 2025.03.26 |
React(리액트) 티키타카 31 (Recharts) (0) | 2025.03.25 |