현재 App에서 todos상태와 onToggle, onRemove, onCreate 함수를 지니고 있게 하고 해당 값들을 props 를 사용해서 자식 컴포넌트들에게 전달해주는 방식으로 구현 할 수 있다.
하지만 이것도 규모가 작아서 망정이지 대규모 프로젝트의 경우에서는 App에서 모든 상태관리를 하기엔 App의 코드가 너무 복잡해질수도 있고, props를 전달해야하는 컴포넌트가 여러 컴포넌트를 거쳐서 전달해야 할 수도 있을 것이다.
이럴 때는 Context API를 활용하면 다음과 같이 구현이 가능하다.
Context API로 dispatch를 바로 참조하는 방법만 다뤘었는데, 이번에는 상태까지고 함께 다루게 된다.
src - TodoContext.js
파일 생성
initialTodos
객체 생성: Todo객체들이 들어었음const initialTodos = [
{
id: 1,
text: '프로젝트 생성하기',
done: true,
},
{
id: 2,
text: '컴포넌트 스타일링하기',
done: true,
},
{
id: 3,
text: 'Context 만들기',
done: false,
},
{
id: 4,
text: '기능 구현하기',
done: false,
}
];
todoReducer(state, action)
: useReducer에 사용될 함수 state와 action을 가져와서 다음상태를 return 한다.function todoReducer(state, action) {
switch(action.type){
case 'CREATE':
return state.concat(action.todo);
case 'TOGGLE':
return state.map(
todo => todo.id === action.id ? {...todo, done: !todo.done} : todo
);
case 'REMOVE':
return state.fileter(
todo => todo.id !==action.id
);
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
}
CREATE : 초기값에서 action.todo 항목을 추가한다.(이때 초기 배열과 레퍼런스는 다름)
TOGGLE : 선택된 할일의 id값과 todo id값들을 비교해서 일치하는 id의 done값을 반대로 변경
REMOVE : filter함수를 이용해서 선택된 id를 제외한 나머지를 todo 항목에 추가한다.
state
와 dispatch
, NextId
각각 context를 제작한다.
const TodoStateContext = createContext();
const TodoDispatchContext = createContext();
export function TodoProvider({children}){
const [state, dispatch] = useReducer(todoReducer, initialTodos);
const nextId = useRef(5);
return (
<TodoStateContext.Provider value={state}>
<TodoDispatchContext.Provider value={dispatch}>
<TodoNextIdContext.Provider value={nextId}>
{children}
</TodoNextIdContext.Provider>
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
)
};
간단하게 TodoState와 TodoDispatch, TodoNextId 쓰기 위해서 제작함
export function useTodoState() {
return useContext(TodoStateContext);
}
export function useTodoDispatch(){
return useContext(TodoDispatchContext);
}
export function useTodoNextId(){
return useContext(TodoNextIdContext);
}
안해도 되지만 습관화 시키는 것이 좋다. 에러처리를 해놓으면 개발하면서 어느 곳에서 에러가 났는지를 수월하게 파악할수 있다. context를 찾을 수 없을때 각각의 hook에서 에러를 나나타게끔 수정한다.
export function useTodoState() {
const context = useContext(TodoStateContext);
if(!context){
throw new Error('Connot find TodoProvider');
}
return context;
}
export function useTodoDispatch(){
const context = useContext(TodoDispatchContext);
if(!context){
throw new Error('Connot find TodoProvider');
}
return context;
}
export function useTodoNextId(){
const context = useContext(TodoNextIdContext);
if(!context){
throw new Error('Connot find TodoProvider');
}
return context;
}
이렇게 수정한뒤에 TodoList
에 useTodoState를 state
라는 변수에 할당한다고 선언을 하면 오류가 발생한다. TodoProvider를 찾을수 없기 때문인데, 이럴 경우에는 App 컴포넌트를 TodoProvider로 감싸면 된다.
function App() {
return (
<TodoProvider>
<GlobalStyle/>
<TodoTemplates>
<TodoHead/>
<TodoList/>
<TodoCreate/>
</TodoTemplates>
</TodoProvider>
);
}
TodoList
에 state가 어떤 것인지 console.log로 출력해보자.
function TodoList() {
const state = useTodoState();
console.log(state);
return (
<TodoListBlock>
<TodoItem text="프로젝트 생성하기" done={true}></TodoItem>
<TodoItem text="컴포넌트 스타일링하기" done={true}></TodoItem>
<TodoItem text="context만들기" done={false}></TodoItem>
<TodoItem text="기능 구현하기" done={false}></TodoItem>
</TodoListBlock>
);
}
결과
context를 2개로 나눈 이유는 나중에 최적화를 하기 위함이고, 사용하기도 간편하다.
TodoCreate에서는 dispatch만 필요한데, 1개의 context 에 state와 dispatch를 모두 넣으면 굳이 렌더링 되지 않아도될 state가 렌더링된다.
context Hook으로 만든 useTodoState()를 이용해서 데이터 배열을 가져온 뒤에 done값이 true인 것들만 따로 추가해서 만든 새로운 배열 undoneTasks
를 제작하고 그 길이값을 반환하여 갯수를 표시하는 방식이다.
function TodoHead() {
const todos = useTodoState();
const undoneTasks = todos.filter(todo => !todo.done)
return (
<TodoHeadBlock>
<h1>2022년 3월 3일 </h1>
<div className="day">일요일</div>
<div className="tasks-left">할 일 {undoneTasks.length}개 남음</div>
</TodoHeadBlock>
);
}
new Date(), toLocaleDateString
에 대한 정보는 아래의 링크를 참고하자.
new Date(), toLocaleDateString (현재 날짜,시간 표시하기 / JavaScript)
dateString
과 dayName
을 선언한다. const today = new Date();
const dateString = today.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
const dayName = today.toLocaleDateString('ko-KR', {
weekday: 'long'
});
TodoHead
의 return 값을 수정한다. 스타일의 적용 여부 확인을 위해 임의로 작성한 날짜와 요일 대신 dateString
과 dayName
으로 수정해주면 된다. return (
<TodoHeadBlock>
<h1>{dateString} </h1>
<div className="day">{dayName}</div>
<div className="tasks-left">할 일 {undoneTasks.length}개 남음</div>
</TodoHeadBlock>
);
현재 상황
TodoContext
의 initialTodos
를 표시한다. 이때 임의연습으로 작성한 할일 목록을 전부 제거하고, 그 대신에 useTodoState
할당 받은 변수를 이용해서 표시할 것이다.
function TodoList() {
const todos = useTodoState();
return (
<TodoListBlock>
{todos.map(
todo => <TodoItem
key={todo.id}
id={todo.id}
text={todo.text}
done={todo.done}
/>
)}
</TodoListBlock>
);
}
적용 확인을 위해 initalTodos
의 text값을 임의로 수정해봤다. 작성자의 경우 4번째 할일의 text를 기능구렁이
로 변경하고 난뒤 새로고침을 해봤다.
제대로 적용된 모습을 확인했다.
onToggle
과 할일 항목을 삭제하는 onRemove
를 제작한다. const dispatch = useTodoDispatch();
const onToggle = () => dispatch({
type: 'TOGGLE',
id
});
const onRemove = () => dispatch({
type: 'REMOVE',
id
});
onToggle
은 CheckCircle props에, onRemove
는 Remove props에 추가한다.TodoItem.js
return (
<TodoItemBlock>
<CheckCircle done={done} onClick={onToggle}>
{done && <MdDone/>}
</CheckCircle>
<Text done={done} >{text}</Text>
<Remove onClick={onRemove}>
<MdDelete/>
</Remove>
</TodoItemBlock>
);
현재 상황
하단의 버튼을 눌러 input
창을 띄우고, 내용을 입력하고 enter를 누르면 항목이 위의 목록에 추가된다.
TodoContext
에 작성한 useTodoDispatch
와 useTodoNextId
를 할당하는 변수를 선언한다. const dispatch = useTodoDispatch();
const nextId = useTodoNextId();
onChange
를 작성한다.const [value, setValue] = useState('');
const onChange = (e) => setValue(e.target.value);
InsertForm
의 styled 종류를 div에서 form으로 변경했다.const InsertFormPositioner = styled.div -> const InsertFormPositioner = styled.form
맨끝의 div를 form으로만 바꾸자.form 형태의 경우는 onSubmit 이벤트를 사용할수 있는데, enter를 누르면 작동하지만, 새로고침이 되어버린다. 따라서 그 새로고침을 막기위해서 preventDefault
를 설정한다. 그와 동시에 dispatch를 'CREATE' 타입으로 작성한다. dispatch로 새로운항목이 추가된 뒤에, input에 입력된 값을 초기화시키고(작성한 항목텍스트를 지운다.) InsertForm
컴퍼넌트를 닫아주기 위해서
open 값을 false로 변경한다.
const onSubmit = (e) => {
e.preventDefault();
dispatch({
type:'CREATE',
todo: {
id: nextId.current,
text: value,
done: false,
}
});
setValue('');
setOpen(false);
nextId.current += 1;
}
TodoCreate
함수의 리턴값을 수정한다. return (
<>
{open && (
<InsertFormPositioner>
<InsertForm onSubmit={onSubmit}>
<Input
placeholder="할일을 입력후, Enter를 누르세요"
autoFocus
onChange={onChange}
value={value}
></Input>
</InsertForm>
</InsertFormPositioner>
)}
<CircleButton open={open} onClick={onToggle}>
<MdAdd />
</CircleButton>
</>
);
}
결과
마지막으로 TodoCreate의 최적화를 위해서 React.memeo를 사용한다.
export default React.memo(TodoCreate);