Language/React.js
[React] TodoList 프로젝트 : React Context
JJcoding
2024. 10. 21. 10:04
Context
- 컴포넌트 간의 데이터를 전달하는 또 다른 방법으로 기존 Props가 가지고 있던 단점을 해결할 수 있다.
- Props의 단점 (Props Drilling) : Props는 부모 → 자식으로만 데이터를 전달 할 수 있다. 컴포넌트의 계층 구조가 두 단계 이상이 되면, 즉 손자 컴포넌트에게는 직접적으로 데이터를 전달 할 수 없다. 중간 다리의 역할을 하는 컴포넌트가 필요하다.
- context는 데이터보관소 역할을 하는 객체이다.
props 사용할 경우
context 사용할 경우
- Context를 사용하기 전 고려할 점
- 컴포넌트와 컨텍스트가 연동되면 재사용성이 떨어진다.
다른 레벨의 많은 컴포넌트가 데이터를 필요로 하는게 아니라면 props를 사용하는게 더 적합하다. - context를 사용하는 대표적인 예 : 현재 지역 정보, UI 테마, 캐싱된 데이터 등
- 컴포넌트와 컨텍스트가 연동되면 재사용성이 떨어진다.
App.jsx
import "./App.css";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";
import { useRef, useReducer, useCallback, createContext } from "react";
const mockDate = [
...생략
];
function reducer(state, action) {
...생략
}
export const TodoContext = createContext();
function App() {
const [todos, dispatch] = useReducer(reducer, mockDate);
const idRef = useRef(3);
const onCreate = useCallback((content) => {
...생략
}, []);
const onUpdate = useCallback((targetId) => {
...생략
}, []);
const onDelete = useCallback((targetId) => {
...생략
}, []);
return (
<div className="App">
<Header />
<TodoContext.Provider
value={{
todos,
onCreate,
onDelete,
onUpdate,
}}
>
<Editor />
<List />
</TodoContext.Provider>
</div>
);
}
export default App;
- context는 대부분 컴포넌트 밖에 선언한다. 컴포넌트가 리렌더링 될때마다 같이 리렌더링 될 필요가 없기 때문에 컴포넌트 밖에 선언한다.
- <TodoContext.Provider> value로 보낼 객체를 명시한다.
Editor.jsx
import "./Editor.css";
import { useState, useRef, useContext } from "react";
import { TodoContext } from "../App";
const Editor = () => {
const { onCreate } = useContext(TodoContext);
const [content, setContent] = useState("");
const contentRef = useRef();
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
if (content === "") {
contentRef.current.focus();
return;
}
onCreate(content);
setContent("");
};
const onKeydown = (e) => {
if (e.keyCode === 13) {
onSubmit();
}
};
return (
<div className="Editor">
<input
ref={contentRef}
value={content}
onKeyDown={onKeydown}
onChange={onChangeContent}
placeholder="새로운 Todo를 입력하세요."
/>
<button onClick={onSubmit}>추가</button>
</div>
);
};
export default Editor;
- 구조 분해 할당으로 onCreate를 꺼내와서 사용한다.
List.jsx
import "./List.css";
import TodoItem from "./TodoItem";
import { useState, useMemo, useContext } from "react";
import { TodoContext } from "../App";
const List = () => {
const [search, setSearch] = useState("");
const { todos } = useContext(TodoContext);
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getFilteredData = () => {
if (search === "") {
return todos;
}
return todos.filter((todo) =>
todo.content.toLowerCase().includes(search.toLowerCase())
);
};
const filteredTodos = getFilteredData();
const { totalCount, doneCount, notDoneCount } = useMemo(() => {
//console.log("getAnalyzedData 호출");
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
}, [todos]);
// 의존성배열 : deps
//const { totalCount, doneCount, notDoneCount } = getAnalyzedData();
return (
<div className="List">
<h4>Todo List📝</h4>
<div>
<div>전체 : {totalCount}</div>
<div>완료 : {doneCount}</div>
<div>진행 : {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요."
/>
<div className="todos_wrapper">
{filteredTodos.map((todo) => {
return <TodoItem key={todo.id} {...todo} />;
})}
</div>
</div>
);
};
export default List;
- 구조 분해 할당으로 todos를 꺼내와서 사용한다.
TodoItem.jsx
import "./TodoItem.css";
import { memo, useContext } from "react";
import { TodoContext } from "../App";
const TodoItem = ({ id, isDone, content, date }) => {
const { onDelete, onUpdate } = useContext(TodoContext);
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
};
export default memo(TodoItem);
- 구조 분해 할당으로 onDelete, onUpdate를 꺼내와서 사용한다.
- 그런데 context를 적용하자, 지난 시간에 했던 최적화가 적용이 안된 모습을 확인할 수 있다. 왜 ?
- <TodoContext.Provider>에서 value props로 보낸 객체가 그 중의 하나의 요소인 todos가 업데이트 되면서 새로운 객체를 생성해서 보내기 때문이다. 그럼 어떻게 해결 할 수 있을까?
- TodoContext를 두 개로 분리하여 공급한다.
App.jsx
import "./App.css";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";
import { useRef, useReducer, useCallback, createContext, useMemo } from "react";
const mockDate = [
...생략
];
function reducer(state, action) {
...생략
}
export const TodoStateContext = createContext();
export const TodoDispatchContext = createContext();
function App() {
const [todos, dispatch] = useReducer(reducer, mockDate);
const idRef = useRef(3);
const onCreate = useCallback((content) => {
...생략
}, []);
const onUpdate = useCallback((targetId) => {
...생략
}, []);
const onDelete = useCallback((targetId) => {
...생략
}, []);
const memoizedDispatch = useMemo(() => {
return { onCreate, onDelete, onUpdate };
}, []);
return (
<div className="App">
<Header />
<TodoStateContext.Provider value={todos}>
<TodoDispatchContext.Provider value={memoizedDispatch}>
<Editor />
<List />
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
</div>
);
}
export default App;
- TodoStateContext 와 TodoDispatchContext 로 나누어서 변경되는 값, 변경되지 않는 값 구분하여 전달한다.
- useMemo를 쓴 이유 ? value props 객체로 함수들을 묶게 되면, 컴포넌트가 리렌더링 될 때마다 묶은 객체가 계속 재생성 될 것이기 때문에 useMemo로 한번만 생성되도록 감싸주었다.
Editor.jsx
import "./Editor.css";
import { useState, useRef, useContext } from "react";
import { TodoDispatchContext } from "../App";
const Editor = () => {
const { onCreate } = useContext(TodoDispatchContext);
const [content, setContent] = useState("");
const contentRef = useRef();
... 생략
List.jsx
import "./List.css";
import TodoItem from "./TodoItem";
import { useState, useMemo, useContext } from "react";
import { TodoStateContext } from "../App";
const List = () => {
const [search, setSearch] = useState("");
const todos = useContext(TodoStateContext);
... 생략
- todos는 값 하나만 그대로 전달 받았기 때문에(객체가 아닌 배열 구조를 그대로 받았다고 보면 됨), 구조분해할당 문법이 아닌 todos로 받으면 된다.
TodoItem.jsx
import "./TodoItem.css";
import { memo, useContext } from "react";
import { TodoDispatchContext } from "../App";
const TodoItem = ({ id, isDone, content, date }) => {
const { onDelete, onUpdate } = useContext(TodoDispatchContext);
... 생략
- 자식 컴포넌트도 자신이 사용하는 context에 따라 새롭게 import 해주면 된다.
출처 : 한입 크기로 잘라먹는 리액트, 처음 만난 리액트