TypeScript 독학 가이드 10 - 실전 프로젝트
이론만 배우면 재미없죠. 실제로 뭔가 만들어봐야 실력이 늘어요. 오늘은 TypeScript로 Todo 앱을 만들어보고, React와 함께 쓰는 방법까지 알아볼게요. 제가 실무에서 쓰는 팁도 함께 공유합니다!
Todo 앱 만들기 (순수 TypeScript)
먼저 Node.js 환경에서 간단한 Todo 앱을 만들어볼게요.
프로젝트 설정
mkdir todo-app
cd todo-app
npm init -y
npm install -g typescript
tsc --init
타입 정의하기
types.ts 파일:
export interface Todo {
id: number;
title: string;
completed: boolean;
createdAt: Date;
}
export type TodoInput = Omit<Todo, "id" | "createdAt">;
export type FilterType = "all" | "active" | "completed";
포인트: Omit으로 입력용 타입을 간단히 만들었어요.
TodoManager 클래스
todoManager.ts 파일:
import { Todo, TodoInput, FilterType } from "./types";
export class TodoManager {
private todos: Todo[] = [];
private nextId: number = 1;
addTodo(input: TodoInput): Todo {
const newTodo: Todo = {
id: this.nextId++,
...input,
createdAt: new Date()
};
this.todos.push(newTodo);
return newTodo;
}
getTodos(filter: FilterType = "all"): Todo[] {
switch (filter) {
case "active":
return this.todos.filter(todo => !todo.completed);
case "completed":
return this.todos.filter(todo => todo.completed);
default:
return this.todos;
}
}
toggleTodo(id: number): boolean {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
return true;
}
return false;
}
deleteTodo(id: number): boolean {
const index = this.todos.findIndex(t => t.id === id);
if (index !== -1) {
this.todos.splice(index, 1);
return true;
}
return false;
}
updateTodo(id: number, updates: Partial<TodoInput>): boolean {
const todo = this.todos.find(t => t.id === id);
if (todo) {
Object.assign(todo, updates);
return true;
}
return false;
}
}
포인트: Partial로 일부 속성만 업데이트 가능하게 했어요.
실행 코드
index.ts 파일:
import { TodoManager } from "./todoManager";
const manager = new TodoManager();
// Todo 추가
manager.addTodo({ title: "TypeScript 공부하기", completed: false });
manager.addTodo({ title: "운동하기", completed: false });
manager.addTodo({ title: "저녁 먹기", completed: true });
// 모든 Todo 조회
console.log("=== 모든 할 일 ===");
manager.getTodos().forEach(todo => {
console.log(`${todo.id}. [${todo.completed ? "✓" : " "}] ${todo.title}`);
});
// 할 일 완료 처리
manager.toggleTodo(1);
// 활성 Todo만 조회
console.log("\n=== 미완료 할 일 ===");
manager.getTodos("active").forEach(todo => {
console.log(`${todo.id}. ${todo.title}`);
});
// 완료된 Todo만 조회
console.log("\n=== 완료된 할 일 ===");
manager.getTodos("completed").forEach(todo => {
console.log(`${todo.id}. ${todo.title}`);
});
실행하기
tsc
node index.js
실행 결과:
✓ 컴파일 성공
=== 모든 할 일 ===
1. [✓] TypeScript 공부하기
2. [ ] 운동하기
3. [✓] 저녁 먹기
=== 미완료 할 일 ===
2. 운동하기
=== 완료된 할 일 ===
1. TypeScript 공부하기
3. 저녁 먹기
타입 안전한 Todo 앱이 완성되었습니다!
React + TypeScript 시작하기
실무에서는 React와 함께 쓰는 경우가 많아요. 프로젝트 생성부터 알아봅시다.
Vite로 프로젝트 생성
npm create vite@latest my-react-app -- --template react-ts
cd my-react-app
npm install
npm run dev
실행 결과:
✓ 프로젝트 생성 완료
VITE v5.0.0 ready in 234 ms
Local: http://localhost:5173/
React + TypeScript 프로젝트가 준비되었습니다
컴포넌트 타입 정의
Todo.tsx:
import { FC } from "react";
interface TodoProps {
id: number;
title: string;
completed: boolean;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
export const Todo: FC<TodoProps> = ({
id,
title,
completed,
onToggle,
onDelete
}) => {
return (
<div style={{ padding: "10px", borderBottom: "1px solid #ccc" }}>
<input
type="checkbox"
checked={completed}
onChange={() => onToggle(id)}
/>
<span style={{ textDecoration: completed ? "line-through" : "none" }}>
{title}
</span>
<button onClick={() => onDelete(id)}>삭제</button>
</div>
);
};
useState 타입 지정
App.tsx:
import { useState } from "react";
import { Todo } from "./Todo";
interface TodoItem {
id: number;
title: string;
completed: boolean;
}
function App() {
const [todos, setTodos] = useState<TodoItem[]>([]);
const [input, setInput] = useState<string>("");
const addTodo = () => {
if (input.trim()) {
setTodos([
...todos,
{ id: Date.now(), title: input, completed: false }
]);
setInput("");
}
};
const toggleTodo = (id: number) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div style={{ padding: "20px", maxWidth: "600px", margin: "0 auto" }}>
<h1>TypeScript Todo App</h1>
<div>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="할 일을 입력하세요"
/>
<button onClick={addTodo}>추가</button>
</div>
<div style={{ marginTop: "20px" }}>
{todos.map(todo => (
<Todo
key={todo.id}
{...todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</div>
</div>
);
}
export default App;
포인트:
- useState에 제네릭으로 타입 지정
- 이벤트 핸들러 타입도 명확히 정의
- 컴포넌트 props는 interface로 정의
이벤트 핸들러 타입
React에서 자주 쓰는 이벤트 타입들:
import { ChangeEvent, FormEvent, MouseEvent } from "react";
function MyComponent() {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("제출됨");
};
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log("클릭됨");
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button onClick={handleClick}>제출</button>
</form>
);
}
Custom Hook 타입 정의
useTodos.ts:
import { useState, useCallback } from "react";
interface Todo {
id: number;
title: string;
completed: boolean;
}
export const useTodos = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = useCallback((title: string) => {
setTodos(prev => [
...prev,
{ id: Date.now(), title, completed: false }
]);
}, []);
const toggleTodo = useCallback((id: number) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
const deleteTodo = useCallback((id: number) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return { todos, addTodo, toggleTodo, deleteTodo };
};
사용:
function App() {
const { todos, addTodo, toggleTodo, deleteTodo } = useTodos();
// ...
}
API 호출 타입 정의
api.ts:
interface User {
id: number;
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
status: number;
message?: string;
}
export async function fetchUsers(): Promise<ApiResponse<User[]>> {
const response = await fetch("https://api.example.com/users");
const data = await response.json();
return {
data,
status: response.status
};
}
export async function fetchUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`https://api.example.com/users/${id}`);
const data = await response.json();
return {
data,
status: response.status
};
}
사용:
const { data: users } = await fetchUsers();
users.forEach(user => {
console.log(user.name); // 타입 안전!
});
타입 안전성 체크 팁
1. strict 모드 켜기
tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
2. ESLint + TypeScript 설정
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
.eslintrc.json:
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
]
}
3. 타입 가드 활용
function isUser(obj: any): obj is User {
return obj && typeof obj.id === "number" && typeof obj.name === "string";
}
const data = await fetchData();
if (isUser(data)) {
console.log(data.name); // 안전하게 사용 가능
}
TypeScript 모범 사례
1. any 사용 최소화
❌ 나쁜 예:
function process(data: any) {
return data.value;
}
✅ 좋은 예:
function process<T extends { value: unknown }>(data: T) {
return data.value;
}
2. 명시적 타입 정의
❌ 나쁜 예:
const users = []; // any[]
✅ 좋은 예:
const users: User[] = [];
3. 타입 재사용
❌ 나쁜 예:
function getUser(id: number): { id: number; name: string } {}
function updateUser(user: { id: number; name: string }) {}
✅ 좋은 예:
interface User {
id: number;
name: string;
}
function getUser(id: number): User {}
function updateUser(user: User) {}
4. 유니온 vs Enum
간단하면 유니온:
type Status = "pending" | "success" | "error";
복잡하면 Enum:
enum Status {
Pending = "PENDING",
Success = "SUCCESS",
Error = "ERROR"
}
5. 타입 좁히기
function processValue(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // string 메서드 사용 가능
}
return value.toFixed(2); // number 메서드 사용 가능
}
실전 프로젝트 체크리스트
프로젝트 시작할 때 이것만은 꼭!
- strict 모드 활성화
- ESLint + TypeScript 설정
- 공통 타입은 별도 파일로 분리
- API 응답 타입 정의
- 에러 처리 타입 정의
- 유틸리티 타입 활용
- any 사용 최소화
- 제네릭으로 재사용성 높이기
마무리
TypeScript 독학 가이드 시리즈가 끝났어요! 지금까지 배운 내용:
- TypeScript 소개 - 왜 필요한지, JavaScript와의 차이
- 환경 설정 - Node.js, TypeScript 설치, 프로젝트 생성
- 기본 타입 - number, string, boolean, array, tuple, enum
- 함수와 타입 - 매개변수, 반환값, 오버로딩
- 인터페이스 - 객체 타입 정의, 확장
- 클래스 - 접근 제한자, 상속, 추상 클래스
- 타입 별칭과 유니온 - type, 유니온, 인터섹션
- 제네릭 - 재사용 가능한 타입 안전 코드
- 유틸리티 타입 - Partial, Pick, Omit 등
- 실전 프로젝트 - Todo 앱, React + TypeScript
이제 실제 프로젝트에 TypeScript를 적용해보세요. 처음엔 타입 에러가 귀찮아도, 한 달만 쓰면 TypeScript 없이는 못 코딩하게 될 거예요. 화이팅!