예전에는 항상 ProfileId가 선택되어 있었지만, 이제는 선택하지 않을 수도 있게 되었습니다.
문제의 크기
거의 모든 서비스 기능이 영향을 받았습니다.
영향받은 주요 기능
콘텐츠 관리 (업로드, 수정, 삭제)
데이터 조회 (통계, 수익, 리포트)
권한 관리 (접근 제어, 팀 관리)
이런 코드가 곳곳에 있었습니다:
typescript
const profileId =getProfileId();// 항상 string이라고 가정api.fetchData(profileId);
기존엔 getProfileId()가 항상 string을 보장했습니다. 하지만 이제는 undefined일 수 있게 되었습니다. 옵셔널이 되어야 합니다.
552개 파일을 수정해야 하는 상황. 어떻게 안전하게 해낼 수 있을까요?
왜 가능했을까? #1: 도메인 분리의 시작점
처음 코드를 작성할 때, 이렇게 할 수도 있었습니다:
typescript
// ❌ 의미 없는 원시 타입const id = cookies.get('ProfileId');// any 타입api.fetchData(id);// 타입 체크 없음
하지만 우리는 이렇게 했습니다:
typescript
// ✅ 도메인 개념을 표현하는 타입typeProfileId=string;const id = cookies.get<ProfileId>('ProfileId');// ProfileId 타입api.fetchData(id);// 타입 안전
무엇이 달라졌을까요?
string은 그냥 원시 타입입니다. "문자열" 이라는 것 외에 아무 의미가 없습니다.
하지만 ProfileId는 다릅니다:
"이 값은 프로필을 식별하는 ID다"
"이 값은 특정 도메인에 속한다"
코드에 의미 계층을 추가한 것입니다.
이 작은 차이가 대규모 변경을 가능하게 만들었습니다.
해결 전략
핵심은 타입 추상화에 있었습니다.
코드 곳곳에 ProfileId 타입이 명시되어 있었기 때문에, 반환 타입만 바꾸면 컴파일러가 수정이 필요한 모든 곳을 알려줄 수 있었습니다.
typescript
// 변경 전constgetProfileId=()=> cookies.get<ProfileId>('ProfileId');// ProfileId 반환// 변경 후constgetProfileId=()=> cookies.get<ProfileId|undefined>('ProfileId');// ProfileId | undefined 반환
타입 하나만 바꿨을 뿐인데, 타입스크립트 컴파일러가 수정이 필요한 모든 곳을 찾아냈습니다.
왜 가능했을까? #2: 변경 추적 가능한 단위 생성
타입 별칭이 없었다면 어땠을까요?
typescript
// 만약 string을 직접 사용했다면constgetProfileId=()=> cookies.get<string>('ProfileId');constfetchData=(id:string)=> api.fetch(id);const userId:string='123';const profileId:string='456';
// Before: 이게 뭔지 알 수 없음functionfetchData(id:string){}// After: ProfileId를 받는구나! 명확함functionfetchData(id:ProfileId){}
2. 변경 추적이 가능해집니다
이제 getProfileId()의 반환 타입만 바꾸면:
typescript
// 이 한 줄만 수정exportconstgetProfileId=()=> cookies.get<ProfileId|undefined>('ProfileId');
TypeScript 컴파일러가 영향받는 모든 곳을 찾아냅니다:
❌ Type 'string | undefined' is not assignable to type 'string'
❌ Argument of type 'string | undefined' is not assignable to parameter
❌ Object is possibly 'undefined'
이 에러들이 수정이 필요한 모든 파일의 위치를 정확히 알려줬습니다.
이것이 "변경 추적 가능한 단위"를 만든 것입니다.
ProfileId 타입 덕분에:
검색: 정확히 관련 코드만 찾음
문서화: 코드 읽을 때 의미가 즉시 파악됨
타입 하나만 바꾸면, 그 변경이 자동으로 전체에 전파되고, 컴파일러가 수정이 필요한 모든 곳을 알려줍니다.
단순히 string이 아니라, ProfileId라는 의미를 부여한 것 - 이것이 안전하게 수정할 수 있었습니다.
수정 과정
우선순위별로 분류했습니다:
기획 확인 필요 - ProfileId가 없는 상황을 고려하지 않은 케이스
단순 null 체크 - 옵셔널 체이닝이나 조건문 추가
로직 변경 - 흐름 자체를 수정해야 하는 케이스
패턴 1: 옵셔널 체이닝
typescript
// Beforeconst data = profileId.something;// Afterconst data = profileId?.something;
패턴 2: 필수값 보장하기
path parameter처럼 '반드시 있어야 하는' 경우를 위한 훅을 만들었습니다:
typescript
const useProfileIdByPath =():ProfileId=>{const{ profileId }=useParams<{ profileId:ProfileId}>();if(!profileId)thrownewError('useProfileIdByPath hook must be used within an profileId path',);return profileId;};
기획 논의는 이런 식이었습니다:
나: "컴파일 오류를 해결하다 보니 이 상황에서는 추가적인 화면 기획이 필요해요! 에러 화면이나 비어 있는 화면이 필요합니다."
기획: "아, 그 부분은 생각 못 했네요. 로그인 페이지로 보내주세요."
또는
나: "이 기능 자체에 대한 기획이 빠져 있는 거 같아요! 이 부분 추가로 필요합니다."
하나씩 수정하다 보니 총 552개 파일이 변경되었습니다.
결과
552개 파일. 2주. 사이드 이펙트 0건.
타입 시스템이 QA 역할까지 해주었습니다. 컴파일러가 알려주는 에러를 하나씩 해결하다 보니, 놓칠 수 있었던 엣지 케이스까지 모두 처리할 수 있었습니다.
심지어 기획에서 빠진 부분까지 발견했습니다.
추가 효과: 협업 속도 향상
타입 별칭의 효과는 여기서 끝나지 않습니다.
협업할 때도 차이가 납니다.
typescript
// ❌ 의미 없는 원시 타입functionupdateProfile(id:string, name:string){// id가 UserId인가? ProfileId인가? TeamId인가?// name이 displayName인가? userName인가?}// ✅ 의미 있는 타입functionupdateProfile(id:ProfileId, name:DisplayName){// 명확합니다}
코드 자체가 문서 역할을 합니다. 주석이나 별도 문서 없이도 "이 함수는 ProfileId를 받는구나" 를 즉시 알 수 있습니다.