본문 바로가기

TypeScript

TypeScript 타입스크립트 + 리액트를 활용해 TodoList 만들기 #2

* 아래의 글은 JavaScript 프롤로그 및 목차를 먼저 읽으신 후 읽으시기를 권장합니다.

* 타입스크립트 기본 문법은 타입스크립트 핸드북을 통해 학습하시는 것을 추천드립니다.


본 글에서 다룰 내용

  • Context 준비하기
  • 컴포넌트에서 Context 사용하기

본 글에서 사용하는 기술

  • TypeScript
  • React-Hooks
  • Context-API
  • SCSS
  • Styled-Components

제일 먼저 상태 전용 Context를 만들어보겠습니다.

 

src/contexts/TodosContext.tsx

import { createContext } from 'react';

// 나중에 다른 컴포넌트에서 타입을 불러와서 쓸 수 있도록 내보내겠습니다.
export type Todo = {
  id: number;
  text: string;
  done: boolean;
};

type TodosState = Todo[];

const TodosStateContext = createContext<TodosState | undefined>(undefined);

createContext 함수의 Generics를 사용하여 Context에서 관리할 값의 상태를 설정해줄 수 있습니다.

추 후 Provider를 사용하지 않았을 때에는 Context의 값이 undefined가 되어야 하므로,

<TodosState | undefined>와 같이 Context의 값이 TodosState일 수도 있고, undefined일 수도 있다고 선언해주세요.


액션을 위한 타입을 선언해줍니다.

 

액션들을 위한 타입스크립트 타입들을 선언해주어야 합니다.

  • CREATE: 새로운 항목 생성
  • TOGGLE: done 값 반전
  • REMOVE: 항목 제거

src/contexts/TodosContext.tsx

import { createContext } from 'react';

export type Todo = {
  id: number;
  text: string;
  done: boolean;
};

type TodosState = Todo[];

const TodosStateContext = createContext<TodosState | undefined>(undefined);

type Action =
  | { type: 'CREATE'; text: string }
  | { type: 'TOGGLE'; id: number }
  | { type: 'REMOVE'; id: number };

이렇게 액션들의 타입을 선언해주면, 디스패치를 위한 Context를 만들 때 디스패치 함수의 타입을 설정할 수 있습니다.

 

import { createContext, Dispatch } from 'react';

export type Todo = {
  id: number;
  text: string;
  done: boolean;
};

type TodosState = Todo[];

const TodosStateContext = createContext<TodosState | undefined>(undefined);

type Action =
  | { type: 'CREATE'; text: string }
  | { type: 'TOGGLE'; id: number }
  | { type: 'REMOVE'; id: number };

type TodosDispatch = Dispatch<Action>;
const TodosDispatchContext = createContext<TodosDispatch | undefined>(undefined);

이렇게 Dispatch를 리액트 패키지에서 불러와서 Generic으로 액션들의 타입을 넣어주면, 추후 컴포넌트에서 액션을 디스패치 할 때 액션들에 대한 타입을 검사할 수 있습니다.

예를 들어, 액션에 추가적으로 필요한 값(ex: id, text)이 빠지면 오류가 발생하죠.


리듀서 작성하기

 

src/contexts/TodosContext.tsx

import { createContext, Dispatch } from 'react';

export type Todo = {
  id: number;
  text: string;
  done: boolean;
};

type TodosState = Todo[];

const TodosStateContext = createContext<TodosState | undefined>(undefined);

type Action =
  | { type: 'CREATE'; text: string }
  | { type: 'TOGGLE'; id: number }
  | { type: 'REMOVE'; id: number };

type TodosDispatch = Dispatch<Action>;
const TodosDispatchContext = createContext<TodosDispatch | undefined>(
  undefined
);

function todosReducer(state: TodosState, action: Action): TodosState {
  switch (action.type) {
    case 'CREATE':
      const nextId = Math.max(...state.map(todo => todo.id)) + 1;
      return state.concat({
        id: nextId,
        text: action.text,
        done: false
      });
    case 'TOGGLE':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case 'REMOVE':
      return state.filter(todo => todo.id !== action.id);
    default:
      throw new Error('Unhandled action');
  }
}

TodosProvider 만들기

 

src/contexts/TodosContext.tsx

 

이제 앞서 개발한 TodosStateContext와 TodosDispatchContext의 Provider를 함께 사용한 TodosProvider를 봅시다.

import React, { createContext, Dispatch, useReducer } from 'react';

(...) // (이전 코드 생략)

export function TodosContextProvider({ children }: { children: React.ReactNode }) {
  const [todos, dispatch] = useReducer(todosReducer, [
    {
      id: 1,
      text: 'Context API 배우기',
      done: true
    },
    {
      id: 2,
      text: 'TypeScript 배우기',
      done: true
    },
    {
      id: 3,
      text: 'TypeScript 와 Context API 함께 사용하기',
      done: false
    }
  ]);

  return (
    <TodosDispatchContext.Provider value={dispatch}>
      <TodosStateContext.Provider value={todos}>
        {children}
      </TodosStateContext.Provider>
    </TodosDispatchContext.Provider>
  );
}

TodosContextProvider는 App에서 불러와서 기존 내용을 감싸주어야 하므로 export로 내보내 주어야 합니다.


커스텀 Hooks 두 개 작성

 

src/contexts/TodosContext.tsx

import React, { createContext, Dispatch, useReducer, useContext } from 'react';

(...)
 
export function useTodosState() {
  const state = useContext(TodosStateContext);
  if (!state) throw new Error('TodosProvider not found');
  return state;
}

export function useTodosDispatch() {
  const dispatch = useContext(TodosDispatchContext);
  if (!dispatch) throw new Error('TodosProvider not found');
  return dispatch;
}

만약 함수 내부에서 필요한 값이 유효하지 않다면 에러를 throw 하여 각 Hooks이 반환하는 값의 타입은 언제나 유효하다는 것을 보장받을 수 있습니다.


컴포넌트에서 Context 사용하기

 

이제 위에서 힘들게 만든 Context를 사용해줄 차례입니다.

이후에는 위에 내용보다 가벼우니 걱정하지 마시고, 따라 하시면 됩니다!🥰


TodosContextProvider로 감싸기

 

가장 먼저 해야 할 작업은 위에서 export 해주었던 TodosContextProvider를 App 컴포넌트에서 불러와 기존 내용을 감싸주는 것입니다.

 

src/App.tsx

import React from 'react';
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';
import styled, { createGlobalStyle } from 'styled-components';
import { TodosContextProvider } from './contexts/TodosContext';

const Rootdiv = styled.div`
  display : flex;
  height : 100vh;
  width : 100%;
  justify-content : center;
  align-items : center;
  flex-direction : column;
`;

const GlobalStyle = createGlobalStyle`
  body{
    padding : 0;
    margin : 0;
  }
`;

const App = () => {
  return(
    <>
      <TodosContextProvider>
        <GlobalStyle />
        <Rootdiv>
          <TodoForm />
          <TodoList />
        </Rootdiv>
      </TodosContextProvider>
    </>
  )
}

export default App;

TodoList에서 상태 조회하기

 

그다음에는 TodoList 컴포넌트에서 Context 안의 상태를 조회하여 내용을 렌더링 해보겠습니다.

우리가 위에서 만들었던 커스텀 Hook을 사용하면 정말로 간단하게 처리할 수 있습니다.

 

src/components/TodoList.tsx

import React from 'react';
import TodoItem from './TodoItem';
import '../styles/TodoList.scss';
import { useTodosState } from '../contexts/TodosContext';

function TodoList(){
    const todos = useTodosState();
    return(
        <div className = "todoList">
            {
                todos.length === 0 ? <span>오늘 할 일을 등록하세요!</span>
                : todos.map(todo => (
                    <TodoItem todo = {todo} key = {todo.id} />
                ))
            }
        </div>
    )
}

export default TodoList;

useTodosState를 불러와서 호출하기만 하면 현재 상태를 조회할 수 있습니다!


TodoForm에서 새 항목 등록하기

 

useTodosDispatch Hook 을 통해 dispatch 함수를 받아오고, 액션을 디스 패치합니다.

 

src/components/TodoForm.tsx

import React, { useState } from 'react';
import '../styles/TodoForm.scss';
import { useTodosDispatch } from '../contexts/TodosContext';

function TodoForm(){
    const [value, setValue] = useState('');
    const dispatch = useTodosDispatch();

    const onSubmit = (e : React.FormEvent) => {
        e.preventDefault();
        dispatch({
            type: 'CREATE',
            text: value
        });
        setValue('');
    }

    return(
        <form onSubmit = {onSubmit} className = "todoForm">
            <input
                value = {value}
                placeholder = "무엇을 하실 건가요??"
                onChange = {e => setValue(e.target.value)}
            />
            <button>등록</button>
        </form>
    );
};

export default TodoForm;

여기까지 따라 하셨다면 TodoList 등록이 될 것입니다.

한 번 확인해보세요!😁✌


TodoItem에서 항목 토클 및 제거

import React from 'react';
import '../styles/TodoItem.scss';
import { useTodosDispatch, Todo } from '../contexts/TodosContext';

export type TodoItemProps = {
    todo : Todo;
}

function TodoItem({ todo } : TodoItemProps){
    const dispatch = useTodosDispatch();

    const onToggle = () => {
        dispatch({
            type: 'TOGGLE',
            id: todo.id
        });
    };

    const onRemove = () => {
        dispatch({
            type: 'REMOVE',
            id: todo.id
        });
    };

    return(
        <div className = "todoItem">
            <div className = "ItemIndex">
                <span>{todo.id}</span>
            </div>
            <div className = {`ItemContent ${todo.done ? 'done' : ''}`}>
                <span onClick = {onToggle}>{todo.text}</span>
            </div>
            <div className = "ItemBtn">
                <span onClick = {onRemove}>삭제</span>
            </div>
        </div>
    );
}

export default TodoItem;

이제 TodoList의 모든 구현이 끝났습니다!!

 

브라우저에서 ToDoList를 직접 경험해보세요!


Velopert님의 블로그 글을 다수 참조하였습니다. 더 자세하게 학습하고 싶으시면 아래의 첫번째 링크를 확인해주세요.

 

혹시나 오류가 나거나, 조금 더 효율적인 코드가 있다면 댓글 남겨주세요!🥰

 

긴 글 읽어주셔서 감사합니다😁


참고 문헌 및 사이트

전체 코드는 제 깃헙 링크에서 확인하실 수 있습니다.

TS-ToDoList - https://github.com/Bigstar1108/TS-ToDoList

 

긴 글 읽어주셔서 감사합니다😀