* 아래의 글은 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님의 블로그 글을 다수 참조하였습니다. 더 자세하게 학습하고 싶으시면 아래의 첫번째 링크를 확인해주세요.
혹시나 오류가 나거나, 조금 더 효율적인 코드가 있다면 댓글 남겨주세요!🥰
긴 글 읽어주셔서 감사합니다😁
참고 문헌 및 사이트
- Velopert TypeScript 환경에서 리액트 Context API 제대로 활용하기
- React Context 공식문서
- [React에서 TypeScript 사용하기] #4 Context API
전체 코드는 제 깃헙 링크에서 확인하실 수 있습니다.
TS-ToDoList - https://github.com/Bigstar1108/TS-ToDoList
긴 글 읽어주셔서 감사합니다😀
'TypeScript' 카테고리의 다른 글
TypeScript 타입스크립트 + 리액트를 활용해 TodoList 만들기 #1 (0) | 2020.07.22 |
---|---|
TypeScript 타입스크립트를 사용해 블록체인 개발하기 #2 (0) | 2020.07.20 |
TypeScript 타입스크립트를 사용해 블록체인 개발하기 #1 (0) | 2020.07.20 |
TypeScript tsc-watch 사용하기 (2) | 2020.07.16 |
TypeScript 타입스크립트 세팅 방법 (0) | 2020.07.16 |