타입스크립트 개발자가 흔히 저지르는 실수 50가지와 완벽 해결법

 

타입스크립트 개발자가 흔히 저지르는 실수 50가지와 완벽 해결법

섹션 1 — 기본기라고 착각하기 쉬운 실수들

1. "빠르니까" 그냥 any 사용하기

any는 타입스크립트의 '상관없음' 버튼인데요.

타입이 무의미해지기 전까진 기분이 좋을지 모릅니다.

function add(a: any, b: any) {
  return a + b;
}
add("5", 5); // "55" 🙃

해결책: unknown이나 적절한 타입을 사용하세요.

any는 그냥 단계만 복잡해진 자바스크립트일 뿐입니다.

2. 컴파일러 경고 무시하기

빨간 줄이 보인다면, 타입스크립트가 문자 그대로 당신을 구해주려고 애쓰는 중이거든요.

해결책: 경고를 버그처럼 취급하세요.

오늘의 경고는 내일의 프로덕션 이슈가 됩니다.

3. 스트릭트(strict) 모드 켜지 않기

초보자의 타입스크립트 프로젝트에서 발생하는 버그의 절반은 누군가 "strict": true 설정을 잊었기 때문입니다.

해결책: tsconfig.json"strict": true를 추가하고 뒤도 돌아보지 마세요.

4. undefinednull 혼동하기

이 둘은 엄연히 다른데요.

undefined는 할당되지 않음, null은 의도적으로 비어 있음을 의미합니다.

이 둘을 섞어 쓰면 타입 가드가 엉망이 되거든요.

해결책: undefinednull을 현명하게 사용하세요.

생각 없이 관습을 따르는 것보다 신중한 선택이 낫습니다.

5. ! (non-null assertion) 남용하기

이건 기본적으로 "타입스크립트야, 날 믿어. 거기 값 확실히 있어."라고 말하는 것과 같은데요.

스포일러를 하나 하자면, 사실 값이 없는 경우가 많습니다.

const name = maybeName!;
console.log(name.length); // 💥 런타임 에러

해결책: 대신 타입 좁히기(type narrowing)를 사용하세요.

엄마 잔소리 듣기 싫다고 귀 막는 것처럼 컴파일러의 입을 막지 마세요.

6. 널 병합 연산자 (??) 잊어버리기

0이나 ""(빈 문자열)이 유효한 값일 때 || 사용을 멈추셔야 합니다.

const count = input || 10; // input이 0이면 망가짐
const fixed = input ?? 10; // ✅ 올바름

해결책: 거짓(falsy)이 아니라 정말로 '값이 없을 때(nullish)'를 의미한다면 ??를 사용하세요.

7. 2025년에도 var 사용하기

아직도 var를 쓰고 계신다면, 브라우저도 IE6를 쓰고 계시길 바랍니다.

해결책: let이나 const를 사용하세요.

스코프는 중요하며, 디버깅의 고통을 덜어줄 것입니다.

8. 맹목적인 타입 단언 (as)

as는 컴파일러에게 거짓말을 시작하기 전까지만 훌륭한데요.

const user = {} as User; // 방금 거짓말을 하셨군요.

해결책: 상상이 아닌 검증 후에만 단언하세요.

9. never 타입 잊어버리기

스위치(switch) 문이나 함수에서 "절대 일어날 수 없는 일"이 있다면, 그걸 증명해야 합니다.

function handle(x: "a" | "b") {
  switch (x) {
    case "a": break;
    case "b": break;
    default: const _exhaustive: never = x;
  }
}

해결책: never를 사용하면 컴파일러가 당신의 QA 엔지니어가 되어줍니다.

10. 적절한 타입 대신 object 사용하기

object는 "원시 타입(primitive)이 아닌 모든 것"을 의미하는데, 별로 도움이 안 되거든요.

해결책: 실제 형태를 정의하세요.

Record<string, unknown>이나 인터페이스(interface)가 object보다 언제나 낫습니다.

섹션 2 — 타입 시스템의 장난질

11. typeinterface 혼동하기

이 둘은 90% 비슷하지만, 나머지 10%가 모든 걸 망칠 수 있는데요.

해결책: 확장할 객체에는 interface를 사용하세요.

유니온, 원시 값, 유틸리티에는 type을 사용하면 됩니다.

12. 유니온 타입 과도하게 설계하기

type Result = Success | Failure | NetworkError | Timeout | Sadness

이건 타입을 작성하는 게 아니라 소설을 쓰는 것과 같습니다.

해결책: 유니온은 간결하게 유지하세요.

뇌가 녹아내리기 전에 단순화해야 합니다.

13. 구별된 유니온(discriminated unions) 사용 안 하기

속성을 수동으로 하나하나 체크하고 있다면, 사서 고생하는 중이거든요.

type Response = { kind: "ok"; data: string } | { kind: "error"; msg: string };

function handle(res: Response) {
  if (res.kind === "ok") console.log(res.data);
}

해결책: kind 같은 필드를 사용해서 타입스크립트가 알아서 타입을 좁히게 만드세요.

14. 리터럴 타입 잊어버리기

왜 이렇게 할 수 있는데 status: string을 쓰시나요?

type Status = "loading" | "done" | "error";

해결책: 구체적으로 명시하세요.

string은 실제 상태 머신을 표현하기엔 너무 모호합니다.

15. 타입 가드(Type Guards) 피하기

타입 가드는 타입스크립트를 똑똑하게 만드는 방법입니다.

function isUser(u: any): u is User {
  return u && typeof u.name === "string";
}

해결책: 미리 가드(검사)하고, 빠르게 실패 처리하세요.

16. keyof 사용 안 하기

속성 이름을 두 번씩 타이핑하고 있다면, keyof가 존재하는 이유를 떠올려야 하는데요.

해결책: 키 접근을 자동화하세요. 문자열 처리는 타입스크립트에게 맡기면 됩니다.

17. 유틸리티 타입 무시하기

이렇게 쓸 필요가 있을까요?

type PartialUser = { name?: string; age?: number };

그냥 이렇게 하면 되는데 말이죠.

type PartialUser = Partial<User>;

해결책: Partial, Pick, Omit, Record, ReturnType은 당신의 가장 친한 친구입니다.

18. Pick 남용하기

가끔 너무 열심히 Pick을 하다가 문맥을 잃어버리는 경우가 생기거든요.

해결책: 5개 이상의 필드를 가져와야 한다면, 차라리 새로운 타입을 정의하는 게 낫습니다.

19. Readonly 잊어버리기

불변성은 우리의 친구인데요.

특히 공유된 상태(state)를 다룰 때는 더욱 그렇습니다.

해결책: 변하면 안 되는 것들은 표시해 두세요.

미래의 나 자신이 흘릴 눈물을 닦아줄 겁니다.

20. 제네릭 중첩하기

Promise<Array<Record<string, Map<string, User[]>>>> 같은 코드를 보고 계신가요?

...이제 멈춰야 할 때입니다.

해결책: 타입을 이름 있는 조각들로 나누세요.

당신의 눈(그리고 동료들)이 고마워할 겁니다.

섹션 3 — 고삐 풀린 제네릭

21. 약한 제네릭 제약 조건

function get<T>(arr: T[], index: number) { return arr[index]; }

해결책: 제약 조건을 추가하세요!

T extends objectT extends keyof U 같은 조건이 타입을 정직하게 유지해 줍니다.

22. 제네릭 추론 잊어버리기

타입스크립트가 추론할 수 있는데 굳이 제네릭을 과하게 선언하지 마세요.

해결책: 타입스크립트가 알아낼 수 있다면, 그냥 내버려 두세요.

23. <T = any> 사용하기

any를 기본값으로 두는 건 목적 자체를 무너뜨리는 일입니다.

해결책: 혼돈이 아니라 의미 있는 무언가를 기본값으로 설정하세요.

24. 제네릭 섀도잉 (Shadowing)

type Response<T> = { data: T };
function handle<T>(res: Response<T>) {} // 이름은 같지만 새로운 골칫덩이

해결책: 함수 내부에서 제네릭 이름을 재사용하지 마세요.

타입스크립트는 그걸 싫어하거든요.

25. 함수에서 제네릭 추론 무시하기

반환 타입은 자동으로 추론할 수 있는데요.

과하게 명시하는 것을 멈추세요.

해결책: 명확성이 반드시 필요한 경우가 아니라면, 타입스크립트가 반환 타입을 추론하도록 두세요.

섹션 4 — 함수와 흐름 제어

26. 선택적 파라미터(Optional Parameters) 오용하기

선택적이라는 건 "undefined일 수도 있음"을 의미하지, "신경 안 씀"을 의미하는 게 아닙니다.

해결책: undefined가 당신을 처리하기 전에 당신이 먼저 처리하세요.

27. Function을 타입으로 사용하기

그건 변수에 "물건(thing)"이라는 라벨을 붙이는 것과 마찬가지거든요.

해결책: 항상 (args: Args) => Return 형태의 시그니처를 사용하세요.

28. 오버로드(Overloads) 잊어버리기

때로는 하나의 함수가 여러 시그니처를 깔끔하게 가질 수 있습니다.

해결책: 유니온 타입의 스파게티 코드 대신 오버로드를 사용하세요.

29. void 무시하기

반환 타입은 중요한데요.

void를 무시하면 undefined 체이닝이 발생할 수 있습니다.

해결책: 함수가 반환 없이 실행만 할 때는 void를 명시적으로 사용하세요.

30. 비동기 반환(Async Returns) 오해하기

async 함수는 항상 Promise를 반환합니다.

예외는 없거든요.

해결책: 두 번 감싸지 마세요.

Promise<Promise<T>>는 살려달라는 비명이나 다름없습니다.

섹션 5 — 클래스와 객체 다루기

31. 초기화되지 않은 클래스 속성

class User {
  name: string; // 💥 strict 모드는 이걸 싫어합니다
}

해결책: 초기화하거나 확정 할당(!)을 사용하되, 책임감 있게 사용하세요.

32. readonly가 어울리는 곳에 private 쓰기

모든 필드에 프라이버시가 필요한 건 아닙니다.

때로는 그냥 불변성이 필요할 뿐이거든요.

해결책: 비밀 유지가 아니라 보호가 목적이라면 readonly를 사용하세요.

33. implements 잊어버리기

클래스가 따른다고 주장하는 인터페이스를 implements 하지 않는다면, 그 클래스는 거짓말을 하고 있는 겁니다.

해결책: 암시적인 것보단 명시적인 것이 낫습니다.

34. 서브클래스에서 타입 덮어쓰기

클래스를 "확장(extend)"한다면서 계약을 "위반(break)"하고 계시네요.

해결책: 부모 타입을 약화시키지 마세요. 책임감 있게 확장해야 합니다.

35. 비동기 생성자 (Async Constructors)

new 앞에는 await를 붙일 수 없습니다.

절대로요.

해결책: 대신 정적(static) create() 메서드를 사용하세요.

36. this 컨텍스트 놓치기

화살표 함수는 당신의 바인딩 범죄를 해결하기 위해 존재합니다.

해결책: 콜백이나 이벤트 핸들러에서는 화살표 함수를 선호하세요.

37. 인덱스 시그니처(Index Signatures) 잊어버리기

동적인 키를 저장한다면, 정의해 줘야 하는데요.

interface Dictionary {
  [key: string]: string;
}

해결책: 인덱스 시그니처는 정신건강을 지켜주는 구세주입니다.

38. 내장 객체 잘못 확장하기

ArrayError를 확장할 때는 super()가 필요합니다.

안 그러면 알 수 없는 이유로 망가지거든요.

해결책: 의심스러울 땐 super()를 호출하세요.

섹션 6 — 비동기와 예외 상황들

39. await 잊어버리기

타입스크립트 버그의 절반은 단지 await가 빠져서 발생합니다.

해결책: 비동기인 모든 것에 await를 붙이세요.

시간을 아끼는 게 아니라 정신건강을 잃고 계신 겁니다.

40. 비동기 에러 잡지 않기 (Not catching)

try/catch는 존재하는 이유가 있습니다.

.catch()도 마찬가지고요.

해결책: 처리되지 않은 거절(Unhandled rejections)은 "괜찮은" 게 아닙니다.

41. 모든 것에 enum 사용하기

정적 문자열에 enum은 너무 과한데요.

해결책: as const 객체를 선호하세요. 더 작고, 간단하고, 빠릅니다.

42. unknownany 혼동하기

unknown은 안전함을 증명하라고 강요하지만, any는 맹목적으로 당신을 믿어줍니다.

해결책: 의심스러울 땐 any보다 unknown이 낫습니다.

43. 타입 좁히기(Type Narrowing) 건너뛰기

타입을 좁히지 않는다면, 그건 그냥 추측하고 있는 거거든요.

해결책: in, typeof, instanceof를 사용하세요.

타입스크립트는 그걸 아주 좋아합니다.

44. async = 병렬이라고 가정하기

루프 안에서의 await는 순차적으로 실행됩니다.

당신이 원한 건 Promise.all()이었을 겁니다.

해결책: 맹목적으로 기다리지 말고, 똑똑하게 await 하세요.

45. 배열에서 readonly 무시하기

const arr: readonly number[] = [1, 2, 3];
arr.push(4); // 안 됩니다

해결책: 변형(mutation) 버그를 막기 위해 readonly 배열을 사용하세요.

46. async + forEach

forEach는 기다려주지(await) 않는데요.

그냥 실행만 하고 떠나버립니다.

해결책: forEach 대신 await와 함께 for...of를 사용하세요.

섹션 7 — 설계와 개발자 경험(DX)

47. 타입 내보내기(Export) 누락

타입을 가져와(import) 놓고 내보내는 걸 잊으셨군요.

리팩토링 지옥에 오신 것을 환영합니다.

해결책: 의도적으로 타입을 내보내세요.

48. as const 무시하기

이건 리터럴을 잠가주고 타입을 더 똑똑하게 만들어줍니다.

const roles = ["admin", "user"] as const;
type Role = typeof roles[number];

해결책: as const는 데이터를 정확한 타입으로 바꿔줍니다.

49. 런타임 로직과 타입 로직 섞기

타입스크립트의 타입은 런타임에 사라지는데요.

if 문 안에서 타입이 당신을 구해주길 기대하지 마세요.

해결책: 타입은 컴파일 후에는 존재하지 않습니다.

런타임 입력값은 수동으로 검증하세요.

50. 여전히 자바스크립트라는 사실 잊기

타입스크립트는 타입을 주는 것이지, 초능력을 주는 게 아닙니다.

훌륭한 타입을 가진 나쁜 로직은 여전히 나쁜 로직일 뿐이거든요.

해결책: 타입스크립트는 도구일 뿐, 보모가 아닙니다.


맺음말

타입스크립트는 문서를 읽어서 배우는 게 아니라, 직접 코드를 부러뜨리고 고치면서 배우는 건데요.

제가 여러분을 위해 대부분의 "부러뜨리는" 과정을 대신해 드렸습니다.

타입을 수갑이 아니라 가드레일로 사용하세요.

미래의 당신(그리고 당신의 팀)이 고마워할 것입니다.