투두리스트 - ContextAPI를 활용한 상태관리

정영찬·2022년 3월 4일
0

리액트

목록 보기
39/79

현재 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 한다.
    action(CREATE,TOGGLE,REMOVE)
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 항목에 추가한다.

ContextAPI 제작

statedispatch, NextId 각각 context를 제작한다.

const TodoStateContext = createContext();
const TodoDispatchContext = createContext();

TodoProvider 생성

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>
    )
};

hook 제작

간단하게 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가 렌더링된다.

기능 구현

Todohead

남은 할일 갯수 표시

context Hook으로 만든 useTodoState()를 이용해서 데이터 배열을 가져온 뒤에 done값이 true인 것들만 따로 추가해서 만든 새로운 배열 undoneTasks를 제작하고 그 길이값을 반환하여 갯수를 표시하는 방식이다.

function TodoHead() {
    const todos = useTodoState();
    const undoneTasks = todos.filter(todo => !todo.done)
    return (
        <TodoHeadBlock>
            <h1>202233</h1>
            <div className="day">일요일</div>
            <div className="tasks-left">할 일 {undoneTasks.length}개 남음</div>
        </TodoHeadBlock>
    );
}

현재 날짜 표시

new Date(), toLocaleDateString 에 대한 정보는 아래의 링크를 참고하자.
new Date(), toLocaleDateString (현재 날짜,시간 표시하기 / JavaScript)

  • 현재의 년/월/일 과 요일을 데이터 값을 할당받는 변수 dateStringdayName을 선언한다.
   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 값을 수정한다. 스타일의 적용 여부 확인을 위해 임의로 작성한 날짜와 요일 대신 dateStringdayName으로 수정해주면 된다.
  return (
        <TodoHeadBlock>
            <h1>{dateString} </h1>
            <div className="day">{dayName}</div>
            <div className="tasks-left">할 일 {undoneTasks.length}개 남음</div>
        </TodoHeadBlock>
    );

현재 상황

TodoList

TodoContextinitialTodos를 표시한다. 이때 임의연습으로 작성한 할일 목록을 전부 제거하고, 그 대신에 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를 기능구렁이로 변경하고 난뒤 새로고침을 해봤다.

제대로 적용된 모습을 확인했다.

TodoItem

  • 할일을 완료했을때, 혹은 완료했다고 생각했으나 완료가 되지 않아서 다시 원래대로 되돌릴때 아이콘을 클릭해서 스타일을 변경하는 기능 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>
    );

현재 상황

TodoCreate

하단의 버튼을 눌러 input창을 띄우고, 내용을 입력하고 enter를 누르면 항목이 위의 목록에 추가된다.

  • TodoContext에 작성한 useTodoDispatchuseTodoNextId를 할당하는 변수를 선언한다.
  const dispatch = useTodoDispatch();
  const nextId = useTodoNextId();
  • input에서 입력받는 텍스트에 대한 기능 구현 입력한 값의 상태를 구하기 위한 useState와 입력받을때 마다 값을 최신화 시키는 onChange를 작성한다.
const [value, setValue] = useState('');
 const onChange = (e) => setValue(e.target.value);
  • input 컴퍼넌트를 덮는 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);

profile
개발자 꿈나무

0개의 댓글