| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 크로스브라우징
- 팀협업
- 웹개발
- 이직
- Ai툴
- opengraph
- 아키텍처
- 프론트엔드
- Vue3
- 프론트엔드개발자
- 개발생산성
- SSR
- 자동화
- 공통화
- 서버사이드렌더링
- 컴포넌트
- 유지보수
- Next.js
- 상태관리
- HTML5
- 프레임워크
- react
- SEO
- TypeScript
- 취업난
- 검색최적화
- 모듈화
- 성능최적화
- 코드품질
- JSON-LD
- Today
- Total
프론트엔드 개발자의 기록
TypeScript 실무에서 겪는 어려움과 해결책 본문
TypeScript를 도입하며 코드 리뷰를 하다 보면 반복적으로 마주치는 문제들이 있습니다. 오늘은 실무에서 자주 겪는 TypeScript의 어려움들과 그 해결책을 정리해보고자 합니다.
1. any 타입의 남용과 타입 안전성 포기
문제점
TypeScript를 처음 시작할 때 가장 흔한 실수는 어려운 타입 정의를 만나면 any로 도피하는 것입니다.
// ❌ 나쁜 예
function fetchUserData(): any {
return fetch('/api/user').then(res => res.json());
}
const userData: any = await fetchUserData();
userData.name; // 타입 체크 없음
userData.unknownProperty; // 런타임 에러 가능성
해결책
구체적인 타입을 정의하거나, 최소한 unknown 타입을 사용하여 타입 안전성을 유지합니다.
// ✅ 좋은 예
interface User {
id: number;
name: string;
email: string;
}
function fetchUserData(): Promise<User> {
return fetch('/api/user').then(res => res.json());
}
// 또는 unknown 사용 후 타입 가드 적용
function processUnknownData(data: unknown) {
if (typeof data === 'object' && data !== null && 'name' in data) {
// 이제 data는 { name: unknown } & object 타입
console.log((data as { name: string }).name);
}
}
2. 타입 추론 과신과 명시적 타입 정의 부족
문제점
TypeScript의 타입 추론이 뛰어나다고 해서 모든 경우에 의존하면 예상치 못한 타입 문제가 발생합니다.
// ❌ 문제가 될 수 있는 예
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
}; // TypeScript가 추론하는 타입이 예상과 다를 수 있음
// 나중에 이런 문제 발생 가능
function updateConfig(newConfig: typeof config) {
// config의 타입이 변경되면 이 함수도 영향받음
}
해결책
중요한 인터페이스나 설정 객체는 명시적으로 타입을 정의합니다.
// ✅ 좋은 예
interface AppConfig {
apiUrl: string;
timeout: number;
retries: number;
}
const config: AppConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
function updateConfig(newConfig: Partial) {
// 타입이 명확하고 안정적
}
3. 제네릭 사용의 어려움
문제점
제네릭을 처음 접하면 언제, 어떻게 사용해야 할지 막막합니다.
// ❌ 제네릭이 필요한데 사용하지 않은 예
function getFirstItem(arr: any[]): any {
return arr[0];
}
const numbers = [1, 2, 3];
const first = getFirstItem(numbers); // any 타입 반환
해결책
재사용 가능한 함수나 컴포넌트에서는 제네릭을 적극 활용합니다.
// ✅ 제네릭을 활용한 개선
function getFirstItem<T>(arr: T[]): T | undefined {
return arr[0];
}
const numbers = [1, 2, 3];
const first = getFirstItem(numbers); // number | undefined 타입
// 더 복잡한 예시
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
return response.json();
}
// 사용할 때 타입이 자동으로 추론됨
const userResponse = await fetchData<User>('/api/users');
userResponse.data.name; // 타입 안전
4. Union 타입과 타입 가드 활용 미숙
문제점
Union 타입을 다룰 때 적절한 타입 가드를 사용하지 않아 타입 에러가 발생합니다.
// ❌ 문제가 있는 예
type Status = 'loading' | 'success' | 'error';
interface LoadingState {
status: 'loading';
}
interface SuccessState {
status: 'success';
data: string;
}
interface ErrorState {
status: 'error';
error: string;
}
type AppState = LoadingState | SuccessState | ErrorState;
function handleState(state: AppState) {
// ❌ 타입 에러: Property 'data' does not exist on type 'AppState'
if (state.status === 'success') {
console.log(state.data); // 여전히 타입 에러
}
}
해결책
적절한 타입 가드와 discriminated union을 활용합니다.
// ✅ 개선된 예
function handleState(state: AppState) {
switch (state.status) {
case 'loading':
// state는 LoadingState 타입으로 좁혀짐
console.log('로딩 중...');
break;
case 'success':
// state는 SuccessState 타입으로 좁혀짐
console.log(state.data); // 타입 안전
break;
case 'error':
// state는 ErrorState 타입으로 좁혀짐
console.log(state.error); // 타입 안전
break;
}
}
// 커스텀 타입 가드 활용
function isSuccessState(state: AppState): state is SuccessState {
return state.status === 'success';
}
if (isSuccessState(state)) {
console.log(state.data); // 타입 안전
}
5. 라이브러리 타입 정의 문제
문제점
서드파티 라이브러리의 타입 정의가 없거나 부정확할 때 어떻게 대처해야 할지 모르겠습니다.
// ❌ 타입 정의가 없는 라이브러리 사용
import someLibrary from 'some-library'; // any 타입
someLibrary.someMethod(); // 타입 체크 없음
해결책
모듈 선언을 통해 직접 타입을 정의하거나, 필요한 부분만 타이핑합니다.
// ✅ 타입 정의 파일 생성 (types/some-library.d.ts)
declare module 'some-library' {
interface LibraryOptions {
timeout?: number;
retries?: number;
}
interface LibraryResponse {
success: boolean;
data: any;
}
function someMethod(options?: LibraryOptions): Promise<LibraryResponse>;
export = { someMethod };
}
// 또는 필요한 부분만 타이핑
declare module 'some-library' {
const someLibrary: {
someMethod: (options?: { timeout?: number }) => Promise<any>;
};
export default someLibrary;
}
6. tsconfig.json 설정의 이해 부족
문제점
TypeScript 컴파일러 옵션을 제대로 이해하지 못해 예상치 못한 동작이 발생합니다.
// ❌ 너무 관대한 설정
{
"compilerOptions": {
"target": "es5",
"strict": false,
"noImplicitAny": false,
"noImplicitReturns": false
}
}
해결책
엄격한 타입 체크 옵션을 활성화하고, 프로젝트에 맞는 설정을 점진적으로 적용합니다.
// ✅ 권장 설정
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
7. 런타임과 컴파일 타임의 혼동
문제점
TypeScript의 타입은 컴파일 타임에만 존재한다는 것을 이해하지 못해 런타임 에러가 발생합니다.
// ❌ 런타임에서 타입 체크를 시도
interface User {
name: string;
age: number;
}
function processUser(data: unknown) {
// ❌ 런타임에서 User 타입은 존재하지 않음
if (data instanceof User) { // 에러!
console.log(data.name);
}
}
해결책
런타임 타입 체크를 위한 별도의 로직을 구현합니다.
// ✅ 런타임 타입 가드 구현
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
typeof (data as User).name === 'string' &&
typeof (data as User).age === 'number'
);
}
function processUser(data: unknown) {
if (isUser(data)) {
// 이제 data는 User 타입으로 좁혀짐
console.log(data.name);
}
}
// 또는 라이브러리 활용 (zod, yup 등)
import { z } from 'zod';
const UserSchema = z.object({
name: z.string(),
age: z.number()
});
type User = z.infer<typeof UserSchema>;
function processUser(data: unknown) {
const result = UserSchema.safeParse(data);
if (result.success) {
console.log(result.data.name); // 타입 안전
}
}
8. 복잡한 타입 정의와 가독성 문제
문제점
너무 복잡한 타입 정의로 인해 코드 가독성이 떨어지고 유지보수가 어려워집니다.
// ❌ 가독성이 떨어지는 복잡한 타입
type ComplexType<T extends Record<string, any>, K extends keyof T> = {
[P in K]: T[P] extends infer U
? U extends (...args: any[]) => any
? ReturnType<U>
: U extends object
? ComplexType<U, keyof U>
: U
: never;
};
해결책
타입을 단계별로 분해하고, 의미 있는 이름을 부여합니다.
// ✅ 가독성을 높인 타입 정의
type FunctionReturnType<T> = T extends (...args: any[]) => any
? ReturnType<T>
: T;
type ProcessObjectProperty<T> = T extends object
? ProcessedObject<T>
: T;
type ProcessedValue<T> = FunctionReturnType<ProcessObjectProperty<T>>;
type ProcessedObject<T extends Record<string, any>> = {
[K in keyof T]: ProcessedValue<T[K]>;
};
// 또는 utility 타입 활용
type UserFields = Pick<User, 'name' | 'email'>;
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;
9. 에러 처리와 타입 안전성
문제점
에러 처리에서 타입 안전성을 고려하지 않아 런타임 에러가 발생합니다.
// ❌ 에러 타입을 고려하지 않은 처리
async function fetchData() {
try {
const response = await fetch('/api/data');
return response.json();
} catch (error) {
// error는 unknown 타입
console.log(error.message); // 타입 에러 가능성
throw error;
}
}
해결책
에러 타입을 명시적으로 처리하고, Result 패턴 등을 활용합니다.
// ✅ 타입 안전한 에러 처리
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = 'ApiError';
}
}
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function fetchData(): Promise<Result<any, ApiError>> {
try {
const response = await fetch('/api/data');
if (!response.ok) {
return {
success: false,
error: new ApiError(response.status, 'API 요청 실패')
};
}
const data = await response.json();
return { success: true, data };
} catch (error) {
return {
success: false,
error: new ApiError(0, '네트워크 오류')
};
}
}
// 사용
const result = await fetchData();
if (result.success) {
console.log(result.data); // 타입 안전
} else {
console.log(result.error.status); // 타입 안전
}
10. 점진적 마이그레이션 전략 부족
문제점
기존 JavaScript 프로젝트를 TypeScript로 마이그레이션할 때 체계적인 전략 없이 진행하여 혼란이 발생합니다.
해결책
단계별 마이그레이션 전략을 수립합니다.
// 1단계: allowJs 옵션으로 시작
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"strict": false
}
}
// 2단계: 핵심 모듈부터 TypeScript로 변환
// utils.ts
export function formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
// 3단계: 점진적으로 strict 옵션 활성화
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"strict": true,
"noImplicitAny": false // 점진적 적용
}
}
// 4단계: 전체 프로젝트 TypeScript 완전 적용
마무리
TypeScript는 강력한 도구이지만, 제대로 활용하려면 시간과 경험이 필요합니다. 위에서 언급한 문제들은 대부분의 개발자가 한 번씩은 겪는 과정이므로, 너무 좌절하지 마시고 점진적으로 개선해나가시기 바랍니다.
특히 중요한 것은:
- any 타입 남용을 피하고 구체적인 타입 정의하기
- 타입 가드와 discriminated union 활용하기
- 런타임과 컴파일 타임의 차이점 이해하기
- 점진적이고 체계적인 도입 전략 수립하기
TypeScript를 마스터하는 것은 하루아침에 되는 일이 아니지만, 꾸준한 학습과 실습을 통해 분명히 개발 생산성과 코드 품질을 크게 향상시킬 수 있습니다.
"타입 안전성은 선택이 아닌 필수입니다. 처음엔 번거로워도, 나중에는 반드시 도움이 됩니다."
'개발기록' 카테고리의 다른 글
| 공통화/모듈화/자동화 설계: 확장 가능한 코드베이스 만들기 (0) | 2025.07.02 |
|---|---|
| 서버 사이드 렌더링(SSR) 완전 정복 (0) | 2025.07.02 |
| 크로스브라우징에 대해 알아보자 (0) | 2025.06.25 |
| 프론트엔드 개발자를 위한 시맨틱 웹- SEO와 사용자 경험을 향상시키는 마크업의 힘 (0) | 2025.06.24 |
| Claude Pro vs 💻 Cursor Pro 차이점 (0) | 2025.06.03 |