☁️ goormTIL | TypeScript #57

매루·2025년 11월 28일

goormTIL

목록 보기
55/67
post-thumbnail

📅 2025-11-28

➡️ TypeScript 타입 단언, 타입 좁히기, 투두리스트 실습에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리


🔎 학습 리마인드

📌 타입 단언 (Type Assertion)

💡 as란?

  • 타입 단언을 위해 사용하는 키워드
  • 타입 추론과는 다르게, 개발자가 명시적으로 타입을 지정하는 방식
  • 타입스크립트가 정확히 타입을 추론할 수 없는 경우 사용

💡 as 사용 목적

  • API 응답, 외부 라이브러리 등에서 타입이 불명확하거나 any로 들어오는 값을 원하는 타입으로 처리할 때
  • 컴파일러가 오류를 발생시켜도 개발자가 정확한 타입을 알고 있다고 확신할 때
const value: any = "Hello";
const length = (value as string).length;

💡 주의점

  • 타입 단언은 타입 안전성을 무시하는 문법 → 잘못 사용하면 런타임 오류 발생 가능
  • 가능한 한 명확한 타입 지정을 사용하고 필요할 때만 사용

📌 모듈과 네임스페이스

  • 모듈: 파일 단위로 코드를 분리하고 가져올 수 있게 함
  • 네임스페이스: 같은 파일 내부에서 논리적으로 코드를 묶어 이름 충돌 방지

💡 모듈(Module)

  • 파일 단위의 코드 분리 및 재사용을 위한 단위
  • export / import로 외부와 기능 공유
  • 프로젝트의 역할을 명확히 분리하고 코드 유지보수를 쉽게 함
// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

// app.ts
import { add, subtract } from "./math";

console.log(add(2, 3)); // 5
console.log(subtract(5, 3)); // 2

💡 네임스페이스(Namespace)

  • 같은 파일 내부에서 논리적 그룹을 만들기 위한 기능
  • 이름 충돌 방지 목적
  • 현재는 ES 모듈 방식이 주로 사용되면서 거의 사용되지 않는 추세
namespace MathUtils {
  export function add(a: number, b: number): number {
    return a + b;
  }

  export function subtract(a: number, b: number): number {
    return a - b;
  }
}

console.log(MathUtils.add(2, 3));
console.log(MathUtils.subtract(5, 3));

💡 타입 가져오기 / 내보내기

  • 일반적인 타입 export/import
    // types/todo.ts
    export type Todo = {
      id: string;
      title: string;
      completed: boolean;
    };
    
    export type ToggleTodo = Omit<Todo, "title">;
    
    // App.tsx
    import { Todo, ToggleTodo } from "./types/todo";
  • 타입 전용 import (import type)
    • TS 3.8 이상부터 지원
    • 타입만 가져온다는 걸 명확히 표현
    • 트리쉐이킹/번들링 최적화에 유리
    • 네임 충돌 방지 및 가독성 개선
    1. 모듈 전체를 타입 전용으로 가져오기

      • 타입만 가져올 때 직관적
      import type { Todo, ToggleTodo } from "./types/todo";
      • 값(value)이 포함되지 않고 온전히 타입만 가져오는 경우에 사용
    2. value + type 섞어 가져올 때

      • import 항목 앞에 type 키워드 개별 지정
      import { type Todo, type ToggleTodo } from "./types/todo";
      • 동일한 라인에서 함수/상수(값)타입을 함께 가져와야 할 때 유용

💡 모듈 보강

  • 외부 라이브러리 타입이 부족하거나 기능을 확장하고 싶을 때 declear module 사용
// 라이브러리 import
import "some-library";

// 모듈 보강
declare module "some-library" {
  export interface SomeType {
    newProperty: string;
  }
}

// 사용
import { SomeType } from "some-library";

const obj: SomeType = {
  newProperty: "Hello",
};

글로벌 타입 선언 (declare global)

  • 전역 객체(window 등)에 타입을 추가해서 사용
declare global {
  interface Window {
    myCustomProperty: string;
  }
}

window.myCustomProperty = "Hello World";
console.log(window.myCustomProperty);
  • declare컴파일러에게 이미 존재하는 타입이라고 알려주는 역할

📌 타입 좁히기 (Type Narrowing)

  • 특정 조건에 따라 변수의 타입을 좁히는 방법
  • 조건문을 통해 변수의 실제 타입을 판별하여 안전한 코드 작성에 도움
  • typeof, instanceof, in, 사용자 정의 타입 가드 등 사용

💡 typeof 타입 가드

  • 기본적으로 가장 많이 사용하는 타입 가드
    function print(value: string | number) {
      if (typeof value === "string") {
        console.log(`String: ${value}`);
      } else {
        console.log(`Number: ${value}`);
      }
    }
    • 기본 자료형 판별에 사용

💡 instanceof 타입 가드

  • 클래스 기반 객체 타입을 판별할 때 사용
    class Dog {
      bark() {
        console.log("Woof!");
      }
    }
    
    class Cat {
      meow() {
        console.log("Meow!");
      }
    }
    
    function makeSound(animal: Dog | Cat) {
      if (animal instanceof Dog) {
        animal.bark();
      } else {
        animal.meow();
      }
    }

💡 in 연산자 타입 가드

  • 특정 속성의 존재 여부로 타입을 좁힘
    interface Fish {
      swim: () => void;
    }
    
    interface Bird {
      fly: () => void;
    }
    
    function move(animal: Fish | Bird) {
      if ("swim" in animal) {
        animal.swim();
      } else {
        animal.fly();
      }
    }

💡 다양한 타입 처리에 활용

  • 복잡한 유니온 타입에서도 안전하게 분기 처리 가능
    function formatValue(value: string | number | boolean) {
      if (typeof value === "string") {
        return value.toUpperCase();
      } else if (typeof value === "number") {
        return value.toFixed(2);
      } else {
        return value ? "TRUE" : "FALSE";
      }
    }
    
    console.log(formatValue("hello"));  // HELLO
    console.log(formatValue(3.14159));  // 3.14
    console.log(formatValue(true));     // TRUE

📌 실습: 투두리스트

💡 타입 정의

  • Todo 타입 정의
    export type Todo = {
        id: string;
        title: string;
        completed: boolean;
    };
  • 완료 여부 토글 별도 타입 생성 (title 제외)
    export type ToggleTodo = Omit<Todo, 'title'>;
  • TodoItem의 props 타입 정의
    export type TodoItemProps = {
        todoList: Todo[];
        onToggle: ({ id, completed }: ToggleTodo) => void;
        onEdit: (id: Todo['id']) => void;
        onDelete: (id: Todo['id']) => void;
    };
    • 투두 데이터 및 함수(토글, 수정, 삭제)

💡 기능 구현

  • 투두리스트 패칭

    const BASE_URL = 'http://localhost:4000/todos';
    const [todos, setTodos] = useState<Todo[]>([]);
    
    const fetchTodos = async () => {
        const res = await fetch(BASE_URL);
    
        if (!res.ok) {
            throw new Error('todo를 받아오는데 실패했습니다.');
        }
    
        const data = await res.json();
    
        return data;
    };
    
    useEffect(() => {
        fetchTodos().then((data) => setTodos(data));
    }, []);
    <ul>
    	  {todos.map((todo) => (
            <li key={todo.id}>
                <span>{todo.completed ? '완료' : '진행중'} | {todo.title}</span>
            </li>
        ))}
    </ul>
    • Todo[] 타입 사용
  • 투두 추가

    const [title, setTitle] = useState<string>('');
    
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setTitle(e.target.value);
    };
    
    // 추가
    const handleAddTodo = async () => {
        const newTodo: Todo = {
            id: crypto.randomUUID(),
            title: title,
            completed: false,
        };
    
        await fetch(BASE_URL, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(newTodo),
        });
    
        setTodos((prev) => [...prev, newTodo]);
        setTitle('');
    };
    <div>
        <input type="text" value={title} onChange={handleChange} />
        <button onClick={handleAddTodo}>추가</button>
    </div>
    • 이벤트 객체 e의 타입 → React.ChangeEvent<HTMLInputElement>

  • 투두 삭제
    const handleDeleteTodo = async (id: Todo['id']) => {
        await fetch(`${BASE_URL}/${id}`, {
            method: 'DELETE',
        });
    
        setTodos((prev) => prev.filter((todo) => todo.id != id));
    };
    <button onClick={() => handleDeleteTodo(todo.id)}>삭제</button>
    • id: Todo['id'] id 타입만 필요할 경우 이렇게 사용할 수도 있음
  • 투두 수정

    const handleEditTodo = async (id: Todo['id']) => {
        const updateTitle = window.prompt('변경할 내용을 입력해 주세요');
    
        if (updateTitle === null) return;
    
        if (!updateTitle.trim()) {
            alert('내용이 비어 있습니다.');
            return;
        }
    
        await fetch(`${BASE_URL}/${id}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ title: updateTitle }),
        });
    
        setTodos((prev) => prev.map((todo) => (todo.id === id ? { ...todo, title: updateTitle } : todo)));
    };
    <button onClick={() => handleEditTodo(todo.id)}>수정</button>
    • id: Todo['id'] 타입 사용
  • 투두 토글

    const handleToggleTodo = async ({ id, completed }: ToggleTodo) => {
        await fetch(`${BASE_URL}/${id}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ completed: !completed }),
        });
    
        setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, completed: !completed } : t)));
    };
    <span onClick={() => handleToggleTodo(todo)}>
        {todo.completed ? '완료' : '진행중'} | {todo.title}
    </span>
    • ToggleTodo 타입 사용
      • idcompleted 타입만 필요한 경우의 타입
  • TodoItems 컴포넌트 분리

    <TodoItems todos={todos} onToggle={handleToggleTodo} onEdit={handleEditTodo} onDelete={handleDeleteTodo} />
    const TodoItems = ({ todos, onToggle, onEdit, onDelete }: TodoItemProps) => {
        return (
            <ul>
                {todos.map((todo) => (
                    <li key={todo.id}>
                        <span onClick={() => onToggle(todo)}>
                            {todo.completed ? '완료' : '진행중'} | {todo.title}
                        </span>
                        <button onClick={() => onEdit(todo.id)}>수정</button>
                        <button onClick={() => onDelete(todo.id)}>삭제</button>
                    </li>
                ))}
            </ul>
        );
    };
    • TodoItemProps 타입 사용
      • 투두 데이터와 토글, 수정, 삭제 함수 타입

0개의 댓글