TypeScript를 이해하고 쓰기 위한 여정, 그리고 성장: 한 입 챌린지 6기 회고

soleil_lucy·2025년 7월 7일
3

회고

목록 보기
4/7

참여 계기

React와 TypeScript로 프로젝트를 하던 중, 문득 이런 생각이 들었습니다.

"나는 이 코드를 진짜 이해하고 쓰는 걸까?"

기능을 구현할 때 종종 다른 사람의 코드를 참고하곤 했지만, 왜 그렇게 작성됐는지 깊이 이해하고 사용하는 경우는 드물었습니다. 일단 작동만 하면 된다는 생각에, 코드의 동작 원리나 타입에 대한 고민은 자주 놓치곤 했습니다.

그래서 이해하고 작성하는 코드를 짜는 개발자가 되고 싶어졌습니다. 그 목표를 이루기 위해 한입 챌린지가 도움이 될 것 같아 참여하게 되었습니다.

또한 혼자 공부할 땐 자주 미루는 편이라, 매일 진도와 미션이 정해진 챌린지를 통해 꾸준히 학습하는 습관을 만들고 싶다는 마음도 컸습니다.

학습 경험

가장 어려웠던 개념

한입 챌린지를 진행하면서 가장 어렵다고 느꼈던 부분은 맵드 타입(Mapped Types), 조건부 타입(Conditional Types), 그리고 infer 키워드였습니다. 이런 문법들은 이번 챌린지를 통해 처음 접한 개념 이기도 하고, 실제 프로젝트에서 써본 적이 거의 없어서 낯설게 느껴졌습니다.

아직은 개념만 이해하는 수준이지만, 어떤 상황에서 유용하게 쓸 수 있을지는 직접 사용해보면서 더 익힐 수 있을 것 같습니다.

맵드 타입 (Mapped Types)

  • 기존 타입의 속성을 반복적으로 변형해 새로운 타입을 만드는 문법입니다.
  • 예: 모든 속성을 readonly 또는 optional로 바꾸고 싶을 때 사용
    interface User {
      id: string;
      name: string;
      age: number;
    }
    
    type PartialUser = {
      [key in keyof User]?: User[key];
    };
    
    type BooleanUser = {
      [key in keyof User]: boolean;
    };
    
    type ReadonlyUser = {
      readonly [key in keyof User]: User[key];
    };

조건부 타입 (Conditional Types)

  • 타입에 따라 다른 타입을 선택하게 해주는 문법입니다.
  • 삼항 연산자처럼 T extends U ? X : Y 형식으로 조건에 따라 타입이 결정됩니다.
  • 예:
    type IsString<T> = T extends string ? true : false;

infer 키워드

  • 조건부 타입 안에서 타입 일부를 추론하여 변수처럼 사용하는 문법입니다.
  • 주로 함수의 반환값 타입을 추출할 때 사용합니다.
  • 예:
    type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

챌린지를 통해 이해하게 된 개념

Day 4 미션에서는 unknown, void, never, any 타입을 정리하는 과제가 있었습니다.

이전에도 몇 번 접했던 타입들이지만, 막연하게만 알고 있었지 언제 어떻게 써야 하는지는 잘 몰랐습니다. 미션을 계기로 공식 문서를 다시 읽고 정리하면서 각 타입의 역할과 언제 사용하는지를 이해하게 되었습니다.

Day 4 미션 보러가기

타입별 정리

  • any:
    • TypeScript의 타입 시스템을 우회하여, 해당 변수에 어떤 타입의 값이든 할당할 수 있도록 허용하는 타입
    • 타입 검사를 하지 않겠다는 의미로 기존의 자바스크립트를 쓰는 것과 동일함
    • 예: 외부 라이브러리 등 타입 정보를 알 수 없는 값을 다룰 시, 일시적으로 타입 안정성을 포기하고 싶을 때 사용, 점진적으로 타입을 적용하는 마이그레이션 과정에서 기존 자바스크립트 코드에 타입을 붙이기 어려울 때 임시로 사용
  • unknown:
    • 정확한 타입을 알 수 없지만 일단 뭔가는 들어올 수 있는 타입
    • 어떤 값이 있을 수는 있지만, 그 값의 타입을 알 수 없음을 나타냄
    • any 와 비슷해 보이지만, unknown 타입의 값은 타입을 확정하지 않으면 사용할 수 없음. 값을 사용하기 전에 타입을 검사 필수
    • 예: 함수에서 유저 입력이나 외부 데이터(예: JSON 파싱 결과 등)을 받을 때, 사용
      function handleInput(data: unknown) {
      	if(typeof data === 'number'){
      		console.log(data.toFixed(2));
      	}
      }
  • never:
    • "절대 반환되지 않는 값"을 의미
    • 예: 정상적으로 종료되지 않거나, 영원히 실행되거나, 오류를 던지는 함수
      function throwError(message: string): never {
      	throw new Error(message);
      }
      
      function loopFunc(): never {
      	while(true) {
      		// ...
      	}
      }
  • void:
    • 반환값이 없다는 걸 의미하는 타입
    • 주로 함수의 반환 타입으로 사용되며, 반환값이 없음을 명확히 함
    • 예: 함수가 아무것도 반환하지 않을 때, 명시적으로 사용, 값을 반환하지 않는 함수(예: 이벤트 핸들러, 로그 함수 등)의 반환 타입 명시적으로 지정할 때 사용, 콜백 함수의 반환 타입
      const arr: number[] = [1, 2, 3];
      const callbackFn = (value: number, index: number): void => {
        console.log(`${index + 1} 번째 요소: ${value}`);
      };
      
      arr.forEach(callbackFn);

예전엔 그냥 ‘이건 안 쓰는 게 좋다더라’ 정도로만 알고 있었지만, 이제는 언제 써야 하고, 어떤 목적을 갖고 있는지 이해하게 되었습니다.

끝까지 파고든 개념: LTS

챌린지 초반 Day 1 강의를 듣던 중, LTS라는 용어가 나왔습니다. 자주 봐온 용어였지만 정확한 의미는 몰랐고, 그 궁금증 덕분에 Node.js 공식 문서까지 찾아가며 끝까지 파고들게 되었습니다.

LTS에 대해 정리한 글 보러가기

LTS, Long Term Support란?

일반적인 소프트웨어 버전보다 훨씬 오랜 기간 동안 안정적인 유지보수와 보안 패치를 제공하는 버전을 의미합니다.

왜 LTS 버전을 쓸까?:

  • 안정성: LTS 버전은 기능이 자주 바뀌지 않아 예측 가능하고 안정적임
  • 업그레이드 부담 감소: 자주 버전을 바꿀 필요 없이, 몇 년 동안 동일한 환경을 유지할 수 있음
  • 보안 유지: 새로운 기능은 없지만, 보안 패치는 계속 제공 되므로 안전하게 사용할 수 있음

Node.js에서의 LTS

Node.js는 정기적으로 새로운 버전을 출시하고, 다음과 같은 주기로 LTS를 운영합니다:

  1. Current(최신 버전)
    • 출시 직후 약 6개월간 최신 기능 테스트 기간
    • 일반 사용자보다는 라이브러리나 도구 제작자나 얼리어답터가 사용
  2. Active LTS (활성 지원)
    • 6개월 후, 짝수 버전만 LTS로 전환
    • 약 18개월간 주요 기능 수정 및 보안 업데이트 제공
    • 실무에 가장 권장되는 시기
  3. Maintenance (유지 관리)
    • 이후 12개월간은 치명적 버그나 보안 이슈만 수정
    • 새로운 기능은 포함되지 않음

나의 변화

챌린지를 시작하기 전의 나에게, 지금의 나를 소개한다면?

지금의 저는 이렇게 말할 수 있을 것 같습니다.

  • TypeScript는 JavaScript로 복잡한 프로그램을 만들면서 유지보수가 어려워졌고, 그 과정에서 정적 타입의 필요성이 커지며 탄생한 언어라는 걸 이해하고 있습니다.
  • TypeScript가 점진적 타입 시스템과 구조적 타입 시스템을 기반으로 동작한다는 걸 알고 있습니다.
  • TypeScript가 제공하는 여러 타입들이 어떤 상황에서 쓰이는 것인지 이해하고 있습니다.

코드를 작성할 때, 달라진 점

예전에는 에러가 나면 무조건 생성형 AI에게 해결책을 묻고, 알려준 코드대로만 고쳤습니다. “일단 에러만 안 나면 되지!”라는 생각이 컸습니다.

하지만 지금은 다릅니다. AI가 제시한 해결 방법을 그대로 적용 하기보다,

  • 왜 이런 해결책을 줬는지,
  • 내 코드 흐름에 어떤 방식이 가장 적합한지 고민한 뒤 스스로 판단하고 적용합니다.

예시: 사용자 정의 타입 가드로 해결한 유니온 타입 문제

챌린지 Day 8의 미션을 조금 수정하여 설명드리겠습니다.
아래 코드는 에러가 나고 있습니다.

type Guest = {
  visitCount: number;
};

type Member = {
  id: string;
};

type User = Guest | Member;

function inviteUser(user: User) {
  console.log(`${user.id}님, 안녕하세요!`);
  // ❌ Error: Property 'id' does not exist on type 'User'. 
  // Property 'id' does not exist on type 'Guest'.(2339)
}

생성형 AI에게 해당 코드와 에러 메시지를 주고 어떻게 해결할지 방법을 물어보겠습니다.

# 프롬프트
너는 타입스크립트를 만든 개발자 앤더스 하일스베르그 (Anders Hejlsberg)야. 내 타입스크립트 코드에 오류가 났는데 어떤 오류인지 해결 방법은 뭐가 있는지 알려줄래?

에러가 나고 있는 코드:

type Guest = {
  visitCount: number;
};
type Member = {
  id: string;
};
type User = Guest | Member;
function inviteUser(user: User) {
  console.log(${user.id}님, 안녕하세요!);
}

에러:
Property 'id' does not exist on type 'User'.
 Property 'id' does not exist on type 'Guest'.(2339)

아래는 생성형 AI에게 받은 답변입니다:

안녕하세요! 이 에러는 유니온 타입(Union Types)을 사용할 때 흔히 발생하는 문제입니다.

**문제 분석:**
`User` 타입은 `Guest | Member`로 정의되어 있는데, TypeScript는 유니온 타입의 모든 멤버에 공통으로 존재하는 속성만 안전하게 접근할 수 있도록 합니다. `Guest` 타입에는 `id` 속성이 없기 때문에 `user.id`에 직접 접근할 수 없습니다.

**해결 방법들:**

1. Type Guard 사용 (권장)

type Guest = {
  visitCount: number;
};
type Member = {
  id: string;
};
type User = Guest | Member;

function inviteUser(user: User) {
  if ('id' in user) {
    // 여기서 user는 Member 타입으로 좁혀집니다
    console.log(`${user.id}님, 안녕하세요!`);
  } else {
    // 여기서 user는 Guest 타입입니다
    console.log(`방문자님, 안녕하세요!`);
  }
}

2. Discriminated Union 사용

type Guest = {
  type: 'guest';
  visitCount: number;
};
type Member = {
  type: 'member';
  id: string;
};
type User = Guest | Member;

function inviteUser(user: User) {
  if (user.type === 'member') {
    console.log(`${user.id}님, 안녕하세요!`);
  } else {
    console.log(`방문자님, 안녕하세요!`);
  }
}

3. Optional Property 사용

type User = {
  id?: string;
  visitCount?: number;
};

function inviteUser(user: User) {
  if (user.id) {
    console.log(`${user.id}님, 안녕하세요!`);
  } else {
    console.log(`방문자님, 안녕하세요!`);
  }
}

가장 안전하고 TypeScript다운 접근법은 **첫 번째 방법(Type Guard)**이나 **두 번째 방법(Discriminated Union)**입니다. 이렇게 하면 컴파일 타임에 타입 안정성을 보장하면서도 런타임에 올바른 로직을 실행할 수 있습니다.

이 에러는 유니온 타입을 사용할 때 발생합니다.

User 타입에는 GuestMember가 포함되어 있고, 두 타입 모두에 공통된 속성이 아닌 id에 접근하려고 했기 때문에 TypeScript는 에러를 내고 있습니다.

예전 같았으면 생성형 AI가 제안한 첫 번째 해결 방법을 그대로 사용하여 에러를 해결했을 겁니다. 하지만 이 방법은 가독성이 떨어질 수 있고, 앞으로 id처럼 공통되지 않은 속성이 더 많아질 경우 코드가 복잡해질 수 있습니다.

그래서 저는 챌린지에서 배운 개념 중 하나인 사용자 정의 타입 가드를 활용해 해결해봤습니다.

// 사용자 정의 타입 가드
function isMember(user: User): user is Member {
  return (user as Member).id !== undefined;
}

function inviteUser(user: User) {
  if(isMember(user)) {
    console.log(`${user.id}님, 안녕하세요!`);
  }
}

이 방식은 userMember 타입인지 명확하게 의도를 드러내면서 타입도 안전하게 좁혀주기 때문에, 가독성과 유지보수 측면 모두에서 더 좋은 선택이라고 생각합니다.

감사의 말

한입 챌린지의 운영 방식 중에서 특히 좋았던 점은 오픈 채팅방을 통한 미션 알림참가자들의 미션 진행 현황표 공유였습니다. 이 덕분에 매일 학습을 놓치지 않고 꾸준히 참여할 수 있었고, 다른 참가자들의 진행 상황을 보며 “나도 뒤처지지 말자”는 마음가짐으로 더 열심히 임할 수 있었습니다.

챌린지를 마친 지금, 하나 떠오른 아이디어가 있습니다. 미션 수행 시 코드를 제출하는 경우가 많았는데, 서로의 코드를 함께 보고 의견을 나눌 수 있는 리뷰 시간이 있었다면 더 좋지 않았을까? 하는 생각이 들었습니다. 실제로 러닝 피라미드(learning pyramid) 모델에서도 토론과 상호작용 기반 학습의 효과가 높다고 알려져 있습니다. 단순히 코드를 완성하는 데 그치지 않고, 서로의 풀이 과정을 공유하고 피드백을 주고받는 시간이 있었다면 학습 효과가 더 높아졌을 것 같습니다.

러닝 피라미드

마지막으로, 이 챌린지를 기획하고 운영해주신 운영진 분들, 그리고 함께 참여해주신 모든 참가자 분들께 진심으로 감사드립니다. 덕분에 약 3주 동안 몰입할 수 있는 환경에서 TypeScript를 이해하고 사용할 수 있는 사람이 되었습니다.

앞으로의 계획

챌린지에서 배운 내용을 실제 프로젝트에 적용해보고 싶습니다. 현재 진행 중인 Receipto라는 애플리케이션(모임 총무를 위한 정산 공유 서비스)을 이번에 학습한 TypeScript 개념들을 바탕으로 리팩터링할 계획입니다.

또한, 이번 챌린지는 강의 기준으로는 섹션 10까지였지만, 실제 강의의 보너스 섹션이 있어 끝까지 들어보려 합니다. React와 TypeScript를 함께 다루는 내용이 포함되어 있어서 React와 TypeScript르 사용하여 프로젝트를 하고 있는 제게 도움이 많이 될 것 같습니다.

그리고 만약 한입 챌린지 7기가 열린다면, 이번에는 React 혹은 Next.js 과정에 참여해보고, 그 배운 내용을 바탕으로 제가 만들고 싶은 서비스를 직접 개발해보는 것이 다음 목표입니다.

참고 자료

profile
여행과 책을 좋아하는 개발자입니다.

0개의 댓글