← 염승준
07

외부 연동 확장 설계: DUPR

2개월 반 걸린 기능을 5일 만에 다른 서비스로 옮긴 이유는 처음의 배치였습니다.

문제
  • 피클볼 국제 레이팅 DUPR 연동(OAuth 계정 연결부터 경기 결과 제출까지)에 약 2개월 반 소요
  • 두 달 뒤 신규 서비스(오픈플레이)에 같은 기능이 필요해짐
해결
  • 처음부터 이벤트 페이지 폴더가 아닌 공용 위치에 두고 화면과 로직을 분리
  • 여섯 개 다이얼로그 흐름은 캡슐화하고 트리거 UI만 Render Props로 개방
  • 도메인 분기는 never로 강제. 오픈플레이 제출 타입을 추가하자 빠뜨린 분기가 실행 전에 컴파일 에러로 드러남
결과
  • 신규 서비스 이식 5일, 커밋 6개. 기존 모듈 9개 파일 그대로 재사용(재사용률 약 65~70%)
  • 다이얼로그는 모두 동적 import라 연동을 쓰지 않는 화면의 추가 번들 0KB
원본 개발
약 2개월 반
신규 도메인 이식
5일 (2026-03-09 ~ 03-13)
이식 커밋
6개
그대로 재사용한 모듈
9개 파일
재사용률
약 65~70%

배경

피클볼 국제 레이팅 시스템인 DUPR 연동을 OAuth 계정 연결부터 경기 결과 제출까지 처음부터 끝까지 만들었습니다. 약 2개월 반이 걸린 작업이었고, 두 달 뒤 신규 서비스인 오픈플레이에 같은 기능이 필요해졌을 때 5일 만에 붙었습니다.

처음에 어디에 둘지

빠르게 붙은 이유는 이식할 때 잘해서가 아니라 처음 만들 때 어디에 둘지를 정해뒀기 때문입니다.

자연스러운 배치는 이벤트 페이지 폴더 안이었지만, 그러면 나중에 꺼내는 리팩토링부터 해야 하고 그게 이식 비용의 대부분이 됩니다. 처음부터 DUPR 로직을 특정 도메인이 아닌 공용 위치(commons/, api/hooks/, hooks/api/)에 두고 화면과 로직을 분리했습니다. 덕분에 오픈플레이에서는 리팩토링 없이 바로 import할 수 있었습니다.

흐름은 안에, 트리거는 밖에

연동 흐름은 소개 → OAuth iframe → 연동 완료 → 클럽 선택 → 클럽 연결 완료로 이어지고, 계정 종류에 따라 갈라집니다. 여섯 개의 다이얼로그를 오가는 이 흐름(단일 경로로는 최대 네 단계)을 한 컴포넌트에 캡슐화하되, 트리거 버튼의 모양과 위치는 쓰는 쪽이 정하도록 Render Props로 열었습니다. 흐름은 어디서 쓰든 같지만 트리거는 화면마다 다르기 때문입니다.

type DUPRConnectProps = {
  connectButton: ({ onClick, onOpenClubSelect }) => ReactNode; // 밖에서 정함
  onComplete?: (type: 'standard' | 'premium' | 'club') => void;
  onClose?: () => void;
  shouldCheckPremium?: boolean;
};

밖에서 받는 건 트리거 UI와 완료 콜백뿐이고, 다이얼로그 여섯 개의 열림 상태와 단계 전이, OAuth iframe, 계정 종류 분기까지 283줄이 전부 안에 있습니다. 이벤트, 오픈플레이, 설정 페이지 어디서든 connectButton과 onComplete만 넘기면 전체 흐름이 동작하고, onComplete가 넘겨주는 연결 종류로 후속 작업의 타입도 보장됩니다. 다이얼로그는 모두 동적 import라 연동을 쓰지 않는 화면에서는 0KB입니다.

도메인이 늘 때 손댈 곳 좁히기

빠뜨린 분기는 컴파일러가 알려 주게

결과 제출 다이얼로그는 type으로 제출 종류(이벤트 DUPR, 오픈플레이 DUPR, 랭크 매치)를 받고, 종류마다 다른 API의 데이터를 씁니다. 나중에 종류가 늘어날 것이라 default에서 남은 값을 never에 할당해 두었습니다.

switch (type) {
  case 'duprSubmit':
    return duprStatus?.eligibility;
  case 'openPlayDuprSubmit':
    return openPlayDuprStatus?.eligibility;
  case 'rankMatchSubmit':
    return rankMatchStatus;
  default: {
    const _exhaustiveCheck: never = type;
    throw new Error(`Unhandled type: ${_exhaustiveCheck}`);
  }
}

오픈플레이용 openPlayDuprSubmit을 추가하자 이 default에서 곧장 컴파일 에러가 났고, 데이터 분기를 빠뜨렸다는 걸 실행하기 전에 알 수 있었습니다. 원리는 블로그 글에 따로 정리했습니다.