최근 취업을 위해 서류를 제출하면서 우대 사항에 TypeScript 에 대한 경험이 많이 적혀있는 것을 보고 공부를 해야겠다 생각하였습니다. 그래서 TypeScript React를 구글링을 하였는데...
~벨로퍼트님... 그는 React의 신인가?..~
벨로퍼트님의 블로그 === React의 라프텔 // True 트루 참트루
검색 결과 이미 1년 전에 벨로퍼트님이 글을 쓰신 게 있어서 이 예제로 튜토리얼을 시작하기로 하였습니다.
하지만 거의 약 1년 전의 글이다 보니 예제의 일부분이 동작하지 않았습니다. 그래서...
벨로퍼트님의 허락을 받고 벨로퍼트님의 예제를 기반으로 한 TodoList를 진행한 경험을 공유하기 위해 글을 쓰게 되었습니다.
혹시나 TypeScript 를 사용함으로써 오는 장점들이나 상세한 내용들은 아래의 링크들에 상세히 설명되어있습니다. 저도 아직 공부하는 입장으로 잘못된 정보를 전달 할 수 있어 링크들로 대체합니다.
이 포스트의 예제 코드는 제 저장소에서 확인하실 수 있습니다.
우선 TodoList를 만들기에 앞서 CRA 프로젝트를 TypeScript 버전으로 생성하겠습니다.
npx create-react-app typescript-velopert-todolist —-typescript
설치 후 src 폴더 내에 필요없는 파일들을 지워주겠습니다.
프로젝트를 생성 하였으니 저희가 만들 TodoList를 보겠습니다.
CSS를 적용하지 않고 할 일 을 입력하면 아래 리스트로 추가되고 리스트를 클릭하여 취소 선을 긋거나 지우기를 클릭하면 할 일을 지우는 웹 애플리케이션을 만들겠습니다.
초록색 박스와 파란색 박스는 컴포넌트를 구분하기 위한 선으로 실제 애플리케이션에는 그려지지 않습니다.
React 웹 애플리케이션을 만들 때 컴포넌트를 생성하는 방법은 2가지가 있습니다.
2개 모두 장 단점이 있으나 저는 2번 방식이 편하므로 상향식 개발을 하겠습니다.
(그래봤자 컴포넌트 2개...)
우선, 파랑색 박스의 TodoItem 부터 만들어 보겠습니다.
src/components/TodoItem.tsx
src 폴더 아래에 components 라는 폴더를 만들고 TodoItem.tsx
라는 파일을 생성해 줍시다.
그 후 아래의 코드를 작성합니다.
import React from 'react';
interface Props {
text: string;
done: boolean;
onToggle(): void;
onRemove(): void;
}
const TodoItem: React.SFC<Props> = ({text, done, onToggle, onRemove}) => (
<li>
<b
onClick={onToggle}
style={{
textDecoration: done ? 'line-through' : 'none'
}}
>
{text}
</b>
<button style={{all: 'unset' , marginLeft: '0.5rem'}} onClick={onRemove}>[지우기]</button>
</li>
);
export default TodoItem;
interface Props 는 컴포넌트에 들어오는 인자에 대한 타입들을 TypeScript 문법으로 미리 선언 한 것입니다.
기존의 React에서는 컴포넌트에서 사용하는 인자들에 대한 정의를 PropTypes로 정의 하였지만 TypeScript는 interface라는 구현체를 구현하여 ToDoItem 컴포넌트에 React.SFC의 Generic으로 주입하여줍니다.
그 후 할 일에 대한 상태들을 관리하기 위해 녹색 박스인 TodoList 컴포넌트를 만들겠습니다.
src/components/TodoList.tsx
역시 src/components 폴더 내부에 TodoList.tsx 파일을 생성해주고 아래의 코드를 작성합니다.
import React from 'react'
import TodoItem from './TodoItem';
interface Props {
}
interface TodoItemState {
id: number;
text: string;
done: boolean;
}
interface State {
input: string;
todoItems: TodoItemState[];
}
class TodoList extends React.Component<Props, State> {
nextTodoId: number = 0;
state:State = {
input: '',
todoItems: []
};
onToggle = (id: number): void => {
const { todoItems } = this.state;
const nextTodoItems:TodoItemState[] = todoItems.map( item => {
if(item.id === id) {
item.done = !item.done
}
return item;
});
this.setState({
todoItems: nextTodoItems
});
}
onSubmit = (e: React.FormEvent<HTMLFormElement>):void => {
e.preventDefault();
const { todoItems, input } = this.state;
const newItem:TodoItemState = { id: this.nextTodoId++, text: input, done: false};
const nextTodoItems:TodoItemState[] = todoItems.concat(newItem);
this.setState({
input: '',
todoItems: nextTodoItems
});
}
onRemove = (id: number): void => {
const { todoItems } = this.state;
const nextTodoItems: TodoItemState[] = todoItems.filter( item => item.id !== id);
this.setState({
todoItems: nextTodoItems
});
}
onChange = (e: React.FormEvent<HTMLInputElement>): void => {
const { value } = e.currentTarget;
this.setState({
input: value
});
}
render() {
const { onSubmit, onChange, onToggle, onRemove } = this;
const { input, todoItems } = this.state;
const todoItemList: React.ReactElement[] = todoItems.map(
todo => (
<TodoItem
key={todo.id}
done={todo.done}
onToggle={() => onToggle(todo.id)}
onRemove={() => onRemove(todo.id)}
text={todo.text}
/>
)
);
return (
<div>
<h1>오늘 뭐하지?</h1>
<form onSubmit={onSubmit}>
<input onChange={onChange} value={input} />
<button type="submit">추가하기</button>
</form>
<ul>
{todoItemList}
</ul>
</div>
);
}
}
export default TodoList;
코드를 다 작성하였으면 index.tsx에 컴포넌트를 추가하여 서버를 실행시켜 주세요.
이상이 TypeScript + React 를 사용한 TodoList 만들기 였습니다.
이제 이 애플리케이션을 Redux를 사용하여 리팩토링 해보겠습니다.
일단 Redux 라이브러리 들을 설치하겠습니다.
yarn add redux react-redux
yarn add --dev @types/react-redux
라이브러리 설치가 끝나면 Redux 모듈을 작성해 보겠습니다.
모듈은 Duck 구조를 사용하여 작성할 것이며, 다음과 같은 순서로 작성합니다.
src/store/modules/todos.ts
// types
export interface TodoItemDataParams {
id: number;
text: string;
done: boolean;
}
export interface TodoState {
todoItems: TodoItemDataParams[];
input: string;
}
export const CREATE = "todo/CREATE";
export const REMOVE = "todo/REMOVE";
export const TOGGLE = "todo/TOGGLE";
export const CHANGE_INPUT = "todo/CHANGE_INPUT";
interface CreateAction {
type: typeof CREATE;
payload: TodoItemDataParams;
}
interface RemoveAction {
type: typeof REMOVE;
meta: {
id: number;
};
}
interface ToggleAction {
type: typeof TOGGLE;
meta: {
id: number;
};
}
interface ChangeInputAction {
type: typeof CHANGE_INPUT;
meta: {
input: string;
};
}
export type TodoActionTypes =
| CreateAction
| RemoveAction
| ToggleAction
| ChangeInputAction;
// actions
let autoId = 0;
function create(text: string) {
return {
type: CREATE,
payload: {
id: autoId++,
text: text,
done: false
}
};
}
function remove(id: number) {
return {
type: REMOVE,
meta: {
id
}
};
}
function toggle(id: number) {
return {
type: TOGGLE,
meta: {
id
}
};
}
function changeInput(input: string) {
return {
type: CHANGE_INPUT,
meta: {
input
}
};
}
export const actionCreators = {
create,
toggle,
remove,
changeInput
};
// reducers
const initialState: TodoState = {
todoItems: [],
input: ""
};
export function todoReducer(
state = initialState,
action: TodoActionTypes
): TodoState {
switch (action.type) {
case CREATE:
return {
input: "",
todoItems: [...state.todoItems, action.payload]
};
case REMOVE:
return {
...state,
todoItems: state.todoItems.filter(todo => todo.id !== action.meta.id)
};
case TOGGLE:
return {
...state,
todoItems: state.todoItems.map(todo => {
if (todo.id === action.meta.id) {
todo.done = !todo.done;
}
return todo;
})
};
case CHANGE_INPUT:
return {
...state,
input: action.meta.input
};
default:
return state;
}
}
모듈을 작성을 하셨으면 Root Reducer를 생성하겠습니다.
src/modules/index.ts
import { combineReducers } from 'redux';
import { TodoState, todoReducer as todo } from './todos';
export interface StoreState {
todos: TodoState;
}
export default combineReducers<StoreState>({
todos
});
이제 스토어를 생성하여 프로젝트에 스토어를 적용 시켜 주겠습니다.
src/store/configureStore.ts
import modules, { StoreState } from "./modules";
import { createStore, Store } from "redux";
export default function configureStore():Store<StoreState> {
const store = createStore(
modules,
(window as any).__REDUX_DEVTOOLS_EXTENSION__ &&
(window as any).__REDUX_DEVTOOLS_EXTENSION__()
);
return store;
}
createStore에서 modules 아래에 추가한 7~8줄의 코드는 redux devtools가 설치되어 있을 시 redux 환경을 브라우저에서 보기 위해 추가하는 코드입니다.
자 스토어를 생성하였으니 이제 Redux를 애플리케이션에 적용하여 보겠습니다.
src/App.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import configureStore from "./store/configureStore";
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
리덕스 모듈로 Todo에 대한 상태를 모두 관리하기 위해 기존의 TodoList 컴포넌트를 상태가 없는 컴포넌트로 리팩토링 하겠습니다.
src/components/TodoList.tsx
import React from "react";
import TodoItem from "./TodoItem";
import { TodoItemDataParams } from "../store/modules/todos";
interface Props {
input: string;
todoItems: TodoItemDataParams[];
onCreate(): void;
onRemove(id: number): void;
onToggle(id: number): void;
onChange(e: any): void;
}
const TodoList: React.SFC<Props> = ({
input,
todoItems,
onCreate,
onRemove,
onToggle,
onChange
}) => {
const todoItemList = todoItems.map(todo =>
todo ? (
<TodoItem
key={todo.id}
done={todo.done}
onToggle={() => onToggle(todo.id)}
onRemove={() => onRemove(todo.id)}
text={todo.text}
/>
) : null);
return (
<div>
<h1>오늘 뭐하지?</h1>
<form onSubmit={(e: React.FormEvent<HTMLElement>) => {
e.preventDefault();
onCreate();
}}>
<input onChange={onChange} value={input} />
<button type="submit">추가하기</button>
</form>
<ul>{todoItemList}</ul>
</div>
);
};
export default TodoList;
이제 TodoList에 상태를 관리할 컨테이너 컴포넌트를 생성하겠습니다.
src/container/TodoListContainer.tsx
import React from 'react';
import TodoList from '../components/TodoList';
import { connect } from 'react-redux';
import { StoreState } from '../store/modules';
import {
TodoItemDataParams,
actionCreators as todosActions,
} from '../store/modules/todos';
import {bindActionCreators} from 'redux';
interface Props {
todoItems: TodoItemDataParams[];
input: string;
TodosActions: typeof todosActions;
}
class TodoListContainer extends React.Component<Props> {
onCreate = (): void => {
const { TodosActions, input } = this.props;
TodosActions.create(input);
}
onRemove = (id: number): void => {
const { TodosActions } = this.props;
TodosActions.remove(id);
}
onToggle = (id: number): void => {
const { TodosActions } = this.props;
TodosActions.toggle(id);
}
onChange = (e: React.FormEvent<HTMLInputElement>): void => {
const { value } = e.currentTarget;
const { TodosActions } = this.props;
TodosActions.changeInput(value);
}
render() {
const { input, todoItems } = this.props;
const { onCreate, onChange, onRemove, onToggle } = this;
return (
<TodoList
input={input}
todoItems={todoItems}
onChange={onChange}
onCreate={onCreate}
onToggle={onToggle}
onRemove={onRemove}
/>
);
}
}
export default connect(
({todos}:StoreState ) => ({
input: todos.input,
todoItems: todos.todoItems
}),
(dispatch) => ({
TodosActions: bindActionCreators(todosActions, dispatch),
})
)(TodoListContainer);
이제 App 컴포넌트에 TodoList를 TodoListContainer로 교체하면 됩니다.
src/App.tsx
import React, { Component } from 'react';
import TodoListContainer from './containers/TodoListContainer';
class App extends Component {
render() {
return (
<div className="App">
<TodoListContainer />
</div>
);
}
}
export default App;
이렇게 TypesSciprt with React + Redux 프로젝트를 완료하였습니다. 이 주제에 대한 더 나은 설명은 벨로퍼트님의 포스팅에 자세히 나와 있으며 전 벨로퍼트님의 예제를 돌아가게끔 다시 작성하였습니다.
저는 Redux에 따로 액션 생성자 유틸 라이브러리를 사용하지 않았으나, 벨로퍼트님의 포스트에서는 redux-actions라는 라이브러리를 사용하셨습니다. 하지만 이 redux-actions 라이브러리는 TypeScript 로 진행 시 오류가 발생하여 따로 액션 생성자 유틸 라이브러리를 사용하지 않고 진행하였습니다.
물론 TypeScript 전용 액션 생성자 유틸 라이브러리인 typesafe-actions 가 있습니다.
다음 블로그 포스팅은 이 라이브러리를 사용하여 Test 까지 진행한 프로젝트를 수행 후 그 경험을 공유하는 글을 작성하도록 하겠습니다.
마지막으로 제가 TypeScript를 사용한 Redux 사용 경험이 적어서 혹시나 저의 redux 구조가 이상하거나 더 나은 방식이 있으시면 댓글로 피드백을 주시면 적극적으로 반영하겠습니다.
긴 글 읽어주셔서 감사합니다.💕
좋은 포스팅 감사합니다.
처음 CRA 로 프로젝트 생성시 커맨드에 오타가있어서 수정요청드립니다.
-typescript
옵션이 --typescript
로 변경되어야 정상적으로 'TypeScript' 로 프로젝트가 생성됩니다.
감사합니다.
포스트 작성 감사합니다.
수정되면 좋을 오타 두개만 댓글로 남깁니다.
1. ReactDOM.render하는 파일은 index.tsx입니다.
2. store/modules/index.ts파일 2번째 라인에
import { TodoState, todoReducer as todos} from './todos'; 입니다.
포스트 작성해주셔서 감사합니다 ^^
이 포스트 업데이트 해야하는데 ㅋㅋ 계속 미뤄지고있네요!