iStack

iStack

여러 언어의 서버 · 클라이언트를
계약 하나로 잇는 모노레포 템플릿

서버 언어 · 프레임워크는 서버마다 앱 웹 · 모바일 · 데스크톱

$ pnpm gen # 서버 코드에서 API 문서와 앱 타입을 만든다

계약이 없으면

서버가 메모 응답의 text 를 body 로 바꿉니다. 앱이 서버를 보고 손으로 적은 타입은 그대로 컴파일되고, 화면에 undefined 가 뜬 뒤에야 누군가 알아챕니다. 서버와 앱이 늘수록 이런 틈은 서로 부르는 짝마다 생깁니다.

// 서버를 보고 손으로 적은 타입
type Note = { id: number; text: string }
const { notes }: { notes: Note[] } = await res.json()
notes[0].text // 화면엔 undefined

서버와 앱은 계약으로만 이어진다

클라이언트와 서버는 서로의 코드를 가져다 쓰지 않고, 계약으로만 이어집니다. 서버는 자기 계약을 내놓을 뿐 누가 부르는지 모르고, 앱은 서버 코드 대신 그 계약을 읽습니다. 서버를 다른 언어로 다시 써도 계약이 같으면 앱은 알 필요가 없습니다.

구조는 왜 이렇게 됐나 →

HTTP 계약은 서버 코드에서

손으로 쓴 문서는 언젠가 코드와 어긋납니다. 그래서 명령 한 번으로 서버 코드에서 OpenAPI 문서를, 그 문서에서 TypeScript 타입을 만듭니다. 같은 변경이 이제 병합 전에 모든 앱의 타입 검사에서 걸립니다.

서버 코드OpenAPI 문서TypeScript 타입앱의 타입 검사

const { notes } = await api.get('/api/notes')
notes[0].text
error TS2339: Property 'text' does not exist on type '{ id: number; body: string; }'.
ADR 0001 · 서버 코드가 기준인 이유 →

서버끼리는 proto 가 먼저

서버끼리 부를 때는 양쪽이 모두 서버입니다. 어느 한쪽의 구현을 기준으로 두는 대신, 공통 proto 하나를 원본으로 두고 서버마다 거기서 코드를 만듭니다. 필드 번호는 다시 쓰지 않습니다. 옛 서버와 새 서버가 함께 도는 동안에도 같은 메시지를 같은 뜻으로 읽어야 하기 때문입니다.

ADR 0002 · 방향이 반대인 이유 →

테스트도 계약을 부른다

테스트는 서버 안의 함수 대신 클라이언트가 부르는 그대로 부릅니다. 실제 서버를 띄워 Docker 의 Postgres 에 붙이고 HTTP · gRPC 로 요청합니다. 테스트가 통과하면 그 API 가 실제로 그렇게 동작하고, 가짜로 바꾸는 것은 외부 서비스뿐입니다.

테스트는 왜 이렇게 됐나 →

PR 하나로 바뀌고, 어긋나면 막힌다

한 저장소라서 API 를 바꾸는 PR 하나에 서버 · 계약 · 클라이언트가 함께 들어가고, 리뷰어는 계약이 바뀐 부분부터 봅니다. CI 는 계약을 다시 만들어 커밋과 같은지, proto 가 호환을 깨지 않는지 확인하고, 에이전트가 생성된 파일을 직접 고치려 하면 편집 전에 막힙니다.

레퍼런스 · 계약 →

공짜는 아니다

엔드포인트 하나에 파일 서너 개와 생성 단계가 따르고, 서버마다 그 언어의 도구와 Docker 가 필요하며, 타입 검사는 생성을 먼저 돌려 느려집니다. 서버가 하나이고 앞으로도 하나라면 이 비용이 더 큽니다. 서버가 둘 이상이 되거나 언어가 섞이기 시작하면 얻는 것이 비용보다 커집니다.

이 구조가 맞지 않는 경우 →

계약을 가운데 둔 구조

  • 계약서버 코드에서 만든 API 문서와 타입, 서버끼리 쓰는 gRPC 정의
  • 서버서버마다 폴더 하나. 언어와 프레임워크는 서버마다 고른다
  • 클라이언트웹 · 모바일 · 데스크톱 앱과 모든 서버를 부르는 공용 클라이언트
  • 인프라개발 · 테스트 · CI 가 함께 쓰는 Postgres 와 전체 스택
iStack 소개 →