
플래닝이 끝나고 난 뒤 저에게 주어진 테스크를 살펴 봅니다. "뭐시기 뭐시기 기능 수정 및 삭제" 큰 기능 아니니 그냥 지워도 될거라는 저의 생각은 조만간 큰 버그를 가져왔습니다.
테스트 서버에 올려보니 직접 페이지를 가야만 터지는 수많은 에러들...undefined is not a function그렇게 미울 수가 없었습니다.
타입스크립트가 null 일 수도 있다는대? 그냥 ! 찍어버리지뭐 하지만 테스트 서버에 올리니 터지는 수많은 에러들...undefined is not a function그렇게 미울 수가 없었습니다.
이거 예전에 만든 타입 같은대 살짝 다르네 그냥 하나더 만들지뭐 (벌써 5개째 만드는 중)
타입스크립트를 사용 했는대 개발 시간만 늘어나고 디버깅 시간만 늘어나는지 어디 좋은 글들 보면 버그가 줄어든다는대 난 왜 버그 투성이에 유지보수 조차 더 힘들어지는 걸까?
"왜 타입스크립트로 개발했는데도 런타임 에러가 끊이지 않을까?"
많은 시간을 들여 타입스크립트로 전환했지만, 여전히 undefined is not a function 에러에 시달리고 계신가요? 대규모 프로젝트에서 타입스크립트의 진정한 가치는 단순한 자동완성이 아닌, 견고한 타입 설계에서 비롯됩니다.
프로젝트를 처음 들어갔을 때, 저는 가장 먼저 확인하는 것 중 하나는 Type입니다.
Type 설계를 보면 데이터의 흐름을 알 수 있고 애플리케이션의 설계에 대해서 이해를 할 수 있다는 기대감 입니다.
하지만 컴파일 오류를 만을 피하기 위한 @ts-ignore, @ts-expect-error, @ts-nocheck과 any, as ... 및 아래와 같은 코드를 보면 머리부터 지끈거리기 시작 합니다.
- 서버 API의 타입은 호출하는 곳에서 그때그때 만들거나
as로 무시- 라이브러리의 제네릭은 무시하고
any로 대체- 도메인 타입이 중복 선언되고 일관성이 없음
- 타입이 기능별로 흩어져 있고, 연결성이 없음
이런 상태에서는 타입이 안정성을 주는 도구가 아닌, 추가적인 관리 부담으로 작용합니다. 개발자들은 점차 타입스크립트의 본질적 가치를 포기하고 "자바스크립트 + 자동완성" 수준으로 타협하게 됩니다.
대부분은 타입이 "설계의 일부"가 아니라 "후처리 대상"이기 때문입니다.
기능 → 구현 → 마지막에 타입 보완
이런 흐름은 결국 타입이 덧붙여지는 존재가 되고, 프로젝트가 커질수록 한계를 드러냅니다.
타입을 코드 완성 후의 "검증 도구"가 아닌, 코드를 작성하기 전의 설계 도구로 접근해야 합니다.
데이터베이스 스키마나 API 문서를 설계하듯, Type 시스템도 프로젝트의 핵심 아키텍처로 다루어야 합니다.
이는 단순히 더 엄격한 TypeScript 설정을 사용하자가 아닌, 타입 주도 설계 방식으로의 전환(Type-Driven-Development)을 의미합니다.
// 안티패턴 1: API 응답에 임시 타입 만들기
function fetchUser() {
return fetch('/api/user')
.then(res => res.json())
.then(data => data as { name: string; email: string }); // API 변경 시 여기도 수동으로 변경해야 함
}
// 안티패턴 2: 같은 도메인 객체에 대한 중복된 타입 정의
// UserProfile.tsx에서
type User = { id: string; name: string; email: string };
// UserSettings.tsx에서 (동일한 유저인데 타입이 다름)
type UserData = { id: string; name: string; emailAddress: string }; // email vs emailAddress
// 안티패턴 3: 타입 불일치를 해결하기 위한 타입 변환의 남용
const userData = fetchUser() as unknown as UserData; // 위험한 타입 캐스팅
다음은 앞으로 전개할 흐름을 간단한 다이어그램으로 표현한 것입니다:
📂 root
├── 📦 Server
│ └─ 📂 [feature]
│ ├── 📂 api
│ └── 📂 types ← 서버 스펙을 타입으로 정의
├── 📦 Shared
│ ├─ 📂 domain ← 도메인 타입, 유스케이스 중심 모델
│ │ └─ 📂 [feature]
│ └─ 📂 lib
│ └─ 📂 api.client
└── 📦 UI
└─ 📄 Component.tsx ← props, state에 타입을 자연스럽게 흐르게
위 구조는 예시이며, 핵심은 "서버 → 도메인 → API → UI"로 타입이 흐르듯 연결되는 설계입니다.
이 시리즈에서는 다음과 같은 방향으로 타입을 중심에 둔 설계를 풀어갈 예정입니다:
단순히 "타입스크립트를 쓰니까 안전하다"가 아닌, 왜 진정한 타입 안전성이 중요한지 살펴봅니다.
// 타입 안전성이 낮은 코드
function processUser(user: any) {
return user.name.toUpperCase(); // 런타임 에러 가능성 높음
}
// 타입 안전성이 높은 코드
function processUser(user: { name: string }) {
return user.name.toUpperCase(); // 컴파일 타임에 안전성 보장
}
Microsoft 연구팀의 논문 To Type or Not to Type: Quantifying Detectable Bugs in JavaScript에 따르면, 타입스크립트는 JavaScript 코드베이스에서 발생할 수 있는 버그의 15%를 사전에 방지할 수 있습니다.
더 흥미로운 것은, Airbnb에서는 타입스크립트 도입으로 발생 가능한 버그의 38%를 사전에 차단할 수 있었다고 보고했습니다.
이러한 데이터는 단순히 타입스크립트를 도입하는 것만으로도 코드 품질이 크게 향상될 수 있음을 보여줍니다.
백엔드와 프론트엔드 사이의 계약인 API부터 타입 정의를 시작하는 방법을 알아봅니다. API 응답 타입을 효과적으로 관리하는 패턴을 소개합니다.
// 📁 types/api.ts - 서버 API 타입 정의의 중심점
export type ApiResponse<T> = {
data: T;
status: number;
message: string;
};
export type UserDTO = {
id: string;
userName: string;
email: string;
createdAt: string;
};
비즈니스 로직에 맞는 도메인 모델을 타입으로 정의하고, API 응답에서 도메인 모델로 변환하는 패턴을 다룹니다. 타입 매핑, 유틸리티 타입, 타입 가드를 활용하여 도메인 중심 설계를 구현하는 방법을 배웁니다.
// 📁 models/user.ts - 도메인 타입 정의
import { UserDTO } from '../types/api';
export type User = {
id: string;
name: string; // userName에서 변환
email: string;
createdAt: Date; // string에서 Date 객체로 변환
};
// 데이터 변환 함수 (DTO → 도메인 모델)
export function toUser(dto: UserDTO): User {
return {
id: dto.id,
name: dto.userName,
email: dto.email,
createdAt: new Date(dto.createdAt),
};
}
API 클라이언트 레이어에서 서버 타입과 도메인 타입을 어떻게 연결하는지 알아봅니다. 중복 없이 타입을 재사용하면서도 관심사를 분리하는 패턴을 제시합니다.
// 📁 api/userApi.ts - API 클라이언트 레이어
import { ApiResponse, UserDTO } from '../types/api';
import { User, toUser } from '../models/user';
export async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: ApiResponse<UserDTO> = await response.json();
if (data.status !== 200) {
throw new Error(data.message);
}
// 서버 응답을 도메인 모델로 변환
return toUser(data.data);
}
UI 컴포넌트에서 props와 state 타입을 도메인 모델과 연결하는 방법을 다룹니다.
// 📁 components/UserProfile.tsx - UI 컴포넌트
import React from 'react';
import { User } from '../models/user';
import { getUser } from '../api/userApi';
type UserProfileProps = {
userId: string;
};
export const UserProfile: React.FC<UserProfileProps> = ({ userId }) => {
const [user, setUser] = React.useState<User | null>(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
async function loadUser() {
try {
const userData = await getUser(userId);
setUser(userData);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}
loadUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>Member since: {user.createdAt.toLocaleDateString()}</p>
</div>
);
};
이 시리즈를 통해 단순한 타입 주석을 넘어서, 전체 애플리케이션 아키텍처를 타입 시스템을 중심으로 설계하는 방법을 배우게 됩니다.
각 레이어에서 타입이 자연스럽게 흐르도록 구성하면, 코드의 안정성은 물론 개발 경험과 유지보수성도 크게 향상됩니다.
타입은 단순히 오류를 막는 보조 도구가 아닙니다.
설계의 일부분이자, 유지보수성과 확장성을 위한 핵심 수단입니다.
이 시리즈는 타입이 프로젝트 전체에서 자연스럽게 흐르도록 설계하는 방법을 다룹니다.
다음 글에서는 서버 API에서 타입을 어떻게 뽑아내고, 어떻게 재사용 구조로 만들 수 있는지 다뤄보겠습니다.
💡 잠깐! 어떤 버전을 읽을지 선택해보세요
이 시리즈는 두 가지 접근 방식으로 구성되어 있습니다:
🚀 빠르고 간단한 함수형 접근
- 대상: 개인 프로젝트, 프로토타입, 소규모 팀 (2-3명)
- 특징: 빠른 구현, 낮은 진입장벽, 직관적
- 👉 함수형 접근 버전 보기
🏗️ 확장 가능한 DI 기반 접근 (현재 글)
- 대상: 팀 프로젝트, 장기 운영, 복잡한 요구사항
- 특징: 테스트 용이성, 환경별 설정, 유지보수성
- 👉 DI 기반 버전 보기
🤔 어떤 걸 선택해야 할지 모르겠다면?
- 테스트가 중요하다면 → DI 기반 접근
- 빠른 프로토타이핑이 목표라면 → 함수형 접근
좋은 글 읽고 갑니다~!
확실히 그때그때 타입을 선언해서 사용하는 것보다, 타입 중심으로 설계하고 시작하면 안정성도 높이고, 불필요한 문제를 겪을 일도 훨씬 줄어들 것 같네요!
타입 정의를 제대로 하지 않고 넘어가면 아슬아슬한 부채가 늘어나는 것 같아요 이렇게 설계부터 다져나가면 견고하게 타입을 관리할 수 있을 것 같네요 ㅎㅎ 잘 읽었습니다
오. 타입스크립트를 코드를 작성하기 전의 설계 도구로 접근하라는 도입부의 말이 와닿네요.
js->ts 넘어갈 때, ts에 대한 지식이 부족한 파트원들과 "우선 개발먼저하고 타입 적용해요!" 하는 의견들로 반영 이후, 굉장히 쓸데없는(?) 후처리과정이 생겼었는지 회상하게 됐어요 .
어디가서 타입스크립트 왜 썼냐고 하면 여기서 읽었던 내용을 좀 인용해봐야겠습니다 ㅎㅎ 사실 서버의 타입과 클라이언트 타입을 분리하고 서버 타입을 가공해서 클라이언트 타입을 사용함에 있어서 같은 타입을 사용해야할지 다른 타입을 생성할지 그리고 다른 타입을 생성한다면 어떻게 관리를 해야할지 고민되는데... 적어주신대로 관리하게 되면 좀 더 관리하기 쉬워지지 않을까 싶습니다 ㅎㅎ 글 잘 읽었습니다!!
글을 읽으면서 많은 공감과 제 자신이 했던 부끄러운 행동들이 생각났습니다
"일단 돌아가게 만들고 나중에 타입 붙이지 뭐" 했다가 나중에 이런 저런 핑계를 대며 그냥 방치했던 기억이 났어요. 특히 API 응답마다 그때그때 타입 만드는 부분이 너무 와닿습니다. 타입을 설계 도구로 접근한다. 배워갑니다. 감사합니다 :)
좋은글 잘 읽었습니다:) 말씀하신대로 타입 시스템을 후처리 과정으로 생각하는 분들이 많은거 같아요...ㅠ 남발되는 any를 보며 한숨 쉬고있는 요즘 팀원들에게 공유 하고싶은 글이네요:)