React TS를 적용하여 간단한 Todo 앱 만들기
TS + React 프로젝트 만들기
프로젝트 github : https://github.com/ykdman/Study/tree/main/React-TS-practice
vite CLI를 이용하여, React + TS + SWC 로 프로젝트를 생성
1
npm create vite@latest
- 옵션 설정
- React 라이브러리 사용
- TypeScript + SWC 옵션 사용
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
{ "name": "react-ts-practice", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react-swc": "^3.5.0", "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", "typescript": "^5.2.2", "vite": "^5.2.0" } }
- @types로 지정된 패키지들은
번역기
역할을 수행한다. - scripts에도 기존 바닐라 JS로 작업할 때 없던
tsc
명령어가 포함되어 있는 것을 알 수 있다.
- @types로 지정된 패키지들은
- vite-env-d.ts
- 개발자가 작성하는 TS 와 바닐라 JS를 연결하는 역할을 한다.
TS + React 로 간단한 ToDo 웹 만들기
- Components 구조로 간단한 ToDo 어플을 만든다.
Props를 TS로 작업하기
React에서 Props를 TS로 작업하기 위해서는 children의 타입까지 동시에 지정해주어야 올바르다.
이에 대해 불편함이 존재하여, TS의 제네릭을 이용하여 컴포넌트의 props 타입을 지정한다.
- 참고 블로그 (TS로 컴포넌트 작업하기)
- https://webruden.tistory.com/931
- https://velog.io/@velopert/create-typescript-react-component
- https://fedev-kim.medium.com/react-react-react-fc-함수형-컴포넌트-타입-e20ddb28a95a
- Interface와 Type의 차이점 참고
- https://yceffort.kr/2021/03/typescript-interface-vs-type
React 함수형 컴포넌트 TS로 만들어보기
- props는 기본적으로 children 속성을 받아 들인다.
- 개발자가 자체적으로 React.FC (함수형 컴포넌트) 명시를 하지 않으면, TS는 해당 함수형 컴포넌트가 함수형 컴포넌트 인지 알 수가 없다.
- 선언하고자 하는 함수형 컴포넌트에
React.FC
제네릭을 지정한다. 위에서 선언한 제네릭 안에 React.PropsWithChildren으로 children을 받아오는 props를 사용함을 제네릭으로 다시 한번 지정한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
import React from "react"; // FC는 Function COmponent => 함수형 컴포넌트를 의미한다. const Todos: React.FC<React.PropsWithChildren<{ items: string[] }>> = ( props ) => { return ( <ul> {props.items.map((item) => ( <li key={item}>{item}</li> ))} {props.children} </ul> ); }; export default Todos;
- 위의 방법을 type을 이용해서 좀 더 깔끔하게 작성하는 방법
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
import React from "react"; // props로 받아야 할 값들을 아래의 type에 지정 (interface도 가능) type TodoProps = { items: string[]; }; // propsWith Children 제네릭으로 위의 TodoProps를 받아오는 제네릭 타입 생성 type PropsWithChildren = React.PropsWithChildren<TodoProps>; // FC는 Function COmponent => 함수형 컴포넌트를 의미한다. // React.FC의 제네릭에 작성한 PropsWithChildren 을 사용하여 children과 TodosProps를 사용가능하게 됐다. const Todos: React.FC<PropsWithChildren> = (props) => { return ( <ul> {props.items.map((item) => ( <li key={item}>{item}</li> ))} {props.children} </ul> ); }; export default Todos;
현재 기준 (2024.06.07) children 을 사용하지 못하는 방식
1 2 3 4 5 6 7 8 9 10 11 12 13
const Todos: React.FC<{ items: string[] }> = (props) => { return ( <ul> {props.items.map((item) => ( <li key={item}>{item}</li> ))} {props.children} </ul> ); }; export default Todos;
- Todos 컴포넌트를 사용하여 children을 사용할 수 없음이 에러 메세지로 표시된다.
하지만 현재 프로젝트를 진행하면서, React.FC에 대해 조금 알아보니, 해당 타입지정자가 없어도 함수형 컴포넌트를 올바르게 작성할 수 있고, 또한 React.FC 자체가 TS와 충돌을 일으킬 가능성이 있기 때문에, 다른 방법으로 함수형 컴포넌트를 작성하였다.
CSS Module 을 위한 설정
vite-end.d.ts 설정파일에 해당 구문을 추가한다.
1 2 3 4 5 6
/// <reference types="vite/client" /> // module CSS 사용을 위한 선언 declare module "*.module.css" { const classes: { [key: string]: string }; export default classes; }
Todo 컴포넌트 작성
- App.tsx 구성
- NewTodo.tsx
- Todo.tsx
- TodoItem.tsx
상태 관리
- context API 를 사용하여 Todo 리스트 관리
todos-context.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
import React, { useState } from "react"; import Todo from "../models/todo"; /* todo.ts class Todo { id: string; text: string; constructor(todoText: string) { this.text = todoText; this.id = new Date().toISOString(); } } export default Todo; */ type TodoContextType = { items: Todo[]; addTodo: (todoText: string) => void; removeTodo: (id: string) => void; }; // 자식 컴포넌트가 사용하게 될 context export const TodoContext = React.createContext<TodoContextType>({ items: [], addTodo: () => {}, removeTodo: (id: string) => { id; }, }); // TodoContextProvider 사용 // React.PropsWithChildren 으로 children 사용 가능 function TodosContextProvider(props: React.PropsWithChildren) { const [todos, setTodos] = useState<Todo[]>([]); function addTodoItem(todoText: string) { setTodos((prevTodos) => { return [...prevTodos, new Todo(todoText)]; }); } function removeTodoItem(id: string) { setTodos((prevTodos) => { return prevTodos.filter((todo) => todo.id !== id); }); } // 자식 컴포넌트가 사용할 context Value 값 const ctxValue: TodoContextType = { items: todos, addTodo: addTodoItem, removeTodo: removeTodoItem, }; return ( <TodoContext.Provider value={ctxValue}> {props.children} </TodoContext.Provider> ); } export default TodosContextProvider;
App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { useContext } from "react";
// import Todo from "../models/todo";
import TodoItem from "./TodoItem";
import classes from "./Todos.module.css";
import { TodoContext } from "../store/todos-context.tsx";
// FC는 Function Component => 함수형 컴포넌트를 의미한다.
function Todos(props: React.PropsWithChildren) {
const TodosCtx = useContext(TodoContext);
return (
<ul className={classes.todos}>
{TodosCtx.items.map((item) => (
<TodoItem
key={item.id}
todoId={item.id}
removeItemHandler={TodosCtx.removeTodo}
>
{item.text}
</TodoItem>
))}
{props.children}
</ul>
);
}
export default Todos;
Todo.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { useContext } from "react";
// import Todo from "../models/todo";
import TodoItem from "./TodoItem";
import classes from "./Todos.module.css";
import { TodoContext } from "../store/todos-context.tsx";
// FC는 Function COmponent => 함수형 컴포넌트를 의미한다.
function Todos(props: React.PropsWithChildren) {
// context 사용
const TodosCtx = useContext(TodoContext);
return (
<ul className={classes.todos}>
{TodosCtx.items.map((item) => (
<TodoItem
key={item.id}
todoId={item.id}
removeItemHandler={TodosCtx.removeTodo}
>
{item.text}
</TodoItem>
))}
{props.children}
</ul>
);
}
export default Todos;
TodoItem.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from "react";
import classes from "./TodoItem.module.css";
type TodoItemProps = {
todoId: string;
removeItemHandler: (id: string) => void;
};
function TodoItem(props: React.PropsWithChildren<TodoItemProps>) {
return (
<li
onClick={() => props.removeItemHandler(props.todoId)}
className={classes.item}
>
{props.children}
</li>
);
}
export default TodoItem;
NewTodo.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { useContext, useRef } from "react";
import classes from "./NewTodo.module.css";
import { TodoContext } from "../store/todos-context";
function NewTodo() {
const TodosCtx = useContext(TodoContext);
const inputTextRef = useRef<HTMLInputElement>(null);
function submitHandler(event: React.FormEvent) {
event.preventDefault();
const enteredText = inputTextRef.current!.value;
if (enteredText.trim().length === 0) {
// error
return;
}
TodosCtx.addTodo(enteredText);
inputTextRef.current!.value = "";
}
return (
<form onSubmit={submitHandler} className={classes.form}>
<label htmlFor="text">Todo Text </label>
<input type="text" id="text" ref={inputTextRef} />
<button>Add Todo</button>
</form>
);
}
export default NewTodo;
Todo 추가 제거 함수 작성
- TodosContext에서 Todo 요소 추가 및 제거 함수를 작성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
type TodoContextType = {
items: Todo[];
addTodo: (todoText: string) => void;
removeTodo: (id: string) => void;
};
function TodosContextProvider(props: React.PropsWithChildren) {
const [todos, setTodos] = useState<Todo[]>([]);
// Todo 추가 함수
function addTodoItem(todoText: string) {
setTodos((prevTodos) => {
return [...prevTodos, new Todo(todoText)];
});
}
// Todo 삭제 함수
function removeTodoItem(id: string) {
setTodos((prevTodos) => {
return prevTodos.filter((todo) => todo.id !== id);
});
}
const ctxValue: TodoContextType = {
items: todos,
addTodo: addTodoItem,
removeTodo: removeTodoItem,
};
return (
<TodoContext.Provider value={ctxValue}>
{props.children}
</TodoContext.Provider>
);
}
tsconfig.json
- target
- TS 컴파일러가 TS 파일을 변환할 떄 타겟으로 삼는 ES 버전 설정
- 최신 브라우저 환경에서는 ES6 로 설정함이 좋고, 구식 브라우저 사용시에는 ES5버전 이하로 낮추어서 지정해주어야 할 수 도있다.
- exnext는 현재 시점 가장 최신의 ES 버전을 지칭한다.
- lib
- TS파일을 JS로 컴파일 할 때, 포함할 라이브러리 목록
- dom : HTML Element와 같은
DOM API
사용을 위해 적용 - dom.iterable
- es2015 : async 문법이나 Promise 를 필요로 할 때 사용
- allowJs
- JS 파일을 컴파일에 포함할 것인지 여부 설정
- JS에서 TS로 점진적으로 전환할 때 사용하면 유용 한 옵션 이다.
- strict
- 타입 확인을 더욱
엄격하게
하기 위할 떄 True로 놓고 사용한다.
- 타입 확인을 더욱
- jsx
- JS 파일에서 JSX구문을 내보내는 방법을 컨트롤 한다.
- include
- 변환할 폴더를 지정
- exclude
- 변환하지 않을 폴더를 지정
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.