leo.dev
backend

공유 패키지와 enum→as const 전환

화면에 뜬 연차 잔여 일수와 서버가 차감한 일수가 달랐다. 같은 입사일, 같은 규칙인데 프론트가 계산한 값과 백엔드가 계산한 값이 어긋났다. 버그는 어느 한쪽의 계산식이 틀려서가 아니라, 같은 계산을 양쪽에 따로 짰기 때문이었다. 한쪽을 고쳐도 다른 쪽은 그대로였다.

날짜 차이, 금액 포맷, 이메일 유효성. 프론트와 백엔드가 똑같이 필요로 하는 로직이 각자의 코드베이스에 독립적으로 구현돼 있었다. 두 구현이 미묘하게 갈라지는 건 시간 문제였고, 연차에서 먼저 터졌다.

타입을 공유하려다 백엔드를 import하게 된다

로직만 문제가 아니었다. 타입 정의도 양쪽에 흩어져 있었다. 백엔드는 엔티티 파일에서 타입을 export했는데, 프론트가 그 타입을 쓰려면 백엔드 코드를 import해야 했다. 타입 하나 가져오자고 엔티티 파일에 딸린 TypeORM 데코레이터·런타임 의존성까지 끌려온다. 잘못 엮이면 순환 의존이 생긴다.

해결의 방향은 분명했다. 런타임 로직도 DB 의존성도 없는, 순수 타입과 상수만 담긴 패키지를 가운데 두고 양쪽이 거기에 의존하게 한다. pnpm workspace로 @repo/common을 만들고 backend·frontend가 똑같이 의존성으로 연결했다. 이제 프론트는 백엔드를 import하지 않는다. 둘 다 가운데 패키지만 본다.

enum을 전부 as const로 갈아엎었다

공유 타입을 옮기면서 TypeScript enum을 전부 as const 객체로 교체했다. 스타일 취향이 아니라 번들 문제다.

enum은 순수 타입이 아니다. 컴파일하면 즉시실행함수(IIFE)로 변환돼 런타임 객체로 남는다. 그래서 트리쉐이킹이 잘 안 먹는다. 한 값도 안 쓰는 enum도 번들에 통째로 실릴 수 있다. 공유 패키지의 상수를 프론트가 import하기 시작하면, 이 군더더기가 프론트 번들로 흘러든다.

// 이전 — 컴파일하면 런타임 IIFE로 남는다
enum UserRole {
ADMIN = 'admin',
MEMBER = 'member',
}
// 이후 — 순수 객체. 안 쓰면 트리쉐이킹으로 사라진다
const UserRole = { ADMIN: 'admin', MEMBER: 'member' } as const;
type UserRole = (typeof UserRole)[keyof typeof UserRole];

as const 객체는 평범한 JS 객체로 컴파일되니 번들러가 안 쓰는 걸 떨어낸다. 값으로도 쓰고(UserRole.ADMIN) 타입으로도 쓰는((typeof UserRole)[keyof typeof UserRole]) 이름이 같은 한 쌍을, 패키지 전체에서 같은 패턴으로 통일했다. 결과적으로 프론트 번들이 줄었다. 안 쓰는 도메인 상수가 더는 실리지 않으니까.

단일 파일이 아니라 도메인별로 나눈다

기존 packages/types는 단일 index.ts에 인증·직원·계약·휴가 타입이 전부 뭉쳐 있었다. 특정 타입을 찾으려면 비대한 파일을 처음부터 뒤져야 했고, 새 타입을 어디에 넣을지도 매번 애매했다.

constants/interfaces/로 가르고, 그 안을 도메인별 파일로 쪼갰다. 계약 상수는 constants/contract.constant.ts, 계약 인터페이스는 interfaces/contract.interface.ts. 유틸 패키지도 같은 원칙으로 날짜·숫자·문자열·유효성 네 영역으로 나눴다. 어디서 찾고 어디에 넣을지가 파일 경로로 결정된다.

import는 barrel로 단순화했다. 경로를 일일이 적지 않고 패키지명만으로 가져온다.

import { UserRole, formatMoney, diffInDays } from '@repo/common';

그래서

처음 터진 건 연차 숫자 하나였지만, 진짜 문제는 “같은 지식이 두 곳에 산다”는 구조였다. 같은 계산식, 같은 enum 값, 같은 포맷 규칙이 프론트와 백엔드에 복제돼 있으면 둘은 반드시 언젠가 갈라진다. 가운데에 순수한 공유 패키지를 두는 건 코드 재사용이 아니라 두 앱이 같은 사실을 보게 만드는 일이다. 그 김에 enum을 걷어내면 프론트 번들도 같이 가벼워진다.

↑↓ 이동 열기esc 닫기