TypeScript 입문 10편 - 실전 프로젝트

(수정: ) learning by Seven Fingers Studio 18분
TypeScript실전프로젝트ReactTodo앱웹개발

typescript guide 10 project

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>
  );
};

typescript guide 10 project

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 독학 가이드 시리즈가 끝났어요! 지금까지 배운 내용:

  1. TypeScript 소개 - 왜 필요한지, JavaScript와의 차이
  2. 환경 설정 - Node.js, TypeScript 설치, 프로젝트 생성
  3. 기본 타입 - number, string, boolean, array, tuple, enum
  4. 함수와 타입 - 매개변수, 반환값, 오버로딩
  5. 인터페이스 - 객체 타입 정의, 확장
  6. 클래스 - 접근 제한자, 상속, 추상 클래스
  7. 타입 별칭과 유니온 - type, 유니온, 인터섹션
  8. 제네릭 - 재사용 가능한 타입 안전 코드
  9. 유틸리티 타입 - Partial, Pick, Omit 등
  10. 실전 프로젝트 - Todo 앱, React + TypeScript

운영자 실전 노트

실제 프로젝트 진행하며 겪은 문제

  • 타입 파일을 어디에 둬야 할지 고민하다 프로젝트 구조 혼란 → types/ 폴더를 만들어 도메인별로 분리하니 관리가 편해짐
  • 공통 타입을 여러 파일에 중복 정의해서 일관성 문제 발생 → 공통 타입은 단일 파일(types/common.ts)에서 관리

이 경험을 통해 알게 된 점

  • 타입 파일 구조화는 프로젝트 초기에 정하는 게 좋다. 나중에 바꾸려면 리팩토링 비용이 크다
  • 도메인별 타입 분리 + 공통 타입 단일 관리 = 유지보수 천국

TypeScript 독학 가이드 시리즈가 끝났다. 이제 실제 프로젝트에 TypeScript를 적용해보자. 처음엔 타입 에러가 귀찮아도, 한 달만 쓰면 TypeScript 없이는 못 코딩하게 될 것이다.

strict 모드를 켜고 시작하는 걸 추천한다. 처음엔 에러가 많이 나지만, 그것이 나중에 버그를 미리 잡아준다. React 프로젝트라면 props 타입만 제대로 정의해도 생산성이 확 올라간다.


다음 글 보기

← 이전 글
TypeScript 독학 가이드 9 - 유틸리티 타입
← 블로그 목록으로