한국어
  • 타입스크립트
  • 모노레포

타입스크립트 올인원: 핵심이고 고통스러운 점과 이익

이 글에서 나는 모노 레포와 폴리 레포를 비교하지 않을 것입니다. 왜냐하면 그것은 모두 철학에 관한 것이기 때문입니다. 대신 나는 건설 및 발전 경험에 집중하고 당신이 JS/TS 생태계에 익숙하다고 가정하겠습니다.

Gao
Gao
Founder

소개

나는 항상 모노레포에 대한 꿈을 가지고 있었습니다.

Airbnb에서 일하면서 모노레포 접근법을 봤는데, 이것은 프론트엔드만을 위한 것이었습니다. 자바스크립트 생태계에 대한 깊은 사랑과 "행복한" 타입스크립트 개발 경험으로, 나는 약 세 년 전부터 프론트엔드와 백엔드 코드를 같은 언어로 일치시키기 시작했습니다. 이것은 대여하기에 좋았지만 (대여하기에 좋았지만) 우리 프로젝트들이 여전히 여러 레포로 흩어져 있어 개발하기가 그렇게 좋지는 않았습니다.

그것이 말하는 것처럼, '프로젝트를 리팩토링하는 최선의 방법은 새로운 것을 시작하는 것이다'. 그래서 내가 약일 년 전에 스타트업을 시작할 때, 나는 완전한 모노 레포 전략을 사용하기로 결정했습니다: 프론트엔드와 백엔드 프로젝트, 심지어 데이터베이스 스키마까지도 모두 한 대여소에 넣어봅니다.

이 글에서 나는 모노 레포와 폴리 레포를 비교하지 않을 것입니다. 왜냐하면 그것은 모두 철학에 관한 것이기 때문입니다. 대신 나는 건설 및 발전 경험에 집중하고 당신이 JS/TS 생태계에 익숙하다고 가정하겠습니다.

최종 결과는 GitHub에서 확인할 수 있습니다.

왜 타입스크립트인가?

솔직히 말해서, 나는 자바스크립트와 타입스크립트의 팬입니다. 나는 그것의 유연성과 엄격함의 호환성을 사랑합니다: 당신은 unknown이나 any로 fallback 할 수 있습니다 (비록 우리가 우리의 코드베이스에서 any의 어떤 형태라도 금지했더라도), 또는 굉장히 엄격한 lint 룰 세트를 사용하여 팀 전체의 코드 스타일을 일치시킬 수 있습니다.

우리가 전에 'fullstack'이라는 개념에 대해 이야기할 때, 우리는 보통 적어도 두 가지의 생태계와 프로그래밍 언어를 상상합니다: 하나는 프론트엔드를 위한 것이고, 다른 하나는 백엔드를 위한 것입니다.

어느 날, 나는 갑자기 그것이 더 간단해질 수 있다는 것을 깨달았습니다: Node.js는 충분히 빠르다 (나를 믿어요, 대부분의 경우, 코드 품질이 실행 속도보다 더 중요합니다), 타입스크립트는 충분히 성숙하다 (큰 프론트엔드 프로젝트에서 잘 작동합니다), 그리고 모노레포 개념은 여러 유명한 팀들 (React, Babel, 등.)에 의해 실습되었습니다 - 그래서 왜 우리는 모든 코드를 함께 결합하지 않고, 프론트엔드에서 백엔드로 이동하는 것일까요? 이것은 엔지니어들이 하나의 리포에서 컨텍스트 스위치 없이 일을 하고, (거의) 하나의 언어로 완전한 기능을 구현할 수 있게 합니다.

패키지 매니저 선택

개발자로서, 그리고 흔히, 나는 코딩을 시작하는 것을 기다릴 수 없었습니다. 하지만 이번에는 뭔가가 달랐습니다.

패키지 매니저의 선택은 모노레포에서 개발 체험에 결정적입니다.

관성의 고통

그것은 2021년 7월이었습니다. 나는 그것을 오랫동안 사용해온 `[email protected]'로 시작했습니다. 요란은 빨랐지만, 곧 나는 요란 작업 공간에 몇 가지 문제를 만났습니다. 예를 들어, 의존성을 정확하게 이포스팅하지 않음, 그리고 수많은 문제들이 '모던에서 수정됨'이라는 태그와 함께 태깅되어 있음, 이것은 나에게 v2 (베리)로 이동하게 합니다.

"좋아, 그럼 지금 업그레이드하겠습니다." 나는 v1와의 투쟁을 멈추고 이주를 시작했습니다. 하지만 베리의 긴 이주 가이드가 나를 무서워하게 했고, 몇 번의 실패한 시도 끝에 나는 포기했습니다.

그냥 작동합니다

그래서 패키지 매니저에 대한 연구가 시작되었습니다. 나는 'pnpm'에 대한 시험 후에 흡수되었습니다: 요란만큼 빠르고, 네이티브 모노레포 지원, npm과 유사한 명령, 하드 링크 등이 있습니다. 가장 중요한 것은, 그것은 그냥 작동합니다. 나는 제품을 시작하는 개발자이지만 패키지 매니저를 개발하지 않는 자라서, 나는 그냥 몇 가지 의존성을 추가하고 패키지 매니저가 어떻게 작동하는지 또는 기타 팬시한 개념을 고려하지 않고 프로젝트를 시작하기를 원했습니다.

같은 생각을 기반으로, 우리는 패키지들을 가로 질러 명령을 실행하고 작업 공간 패키지를 게시하는 데에'레르나'라는 오래된 친구를 선택했습니다.

패키지 범위 정의

처음부터 각 패키지의 최종 범위를 명확하게 파악하는 것은 어렵습니다. 당신의 최선의 시도로 현재 상황에 따라 시작하고, 개발 중에는 언제든지 리팩토링할 수 있다는 것을 기억하세요.

우리의 초기 구조는 네 가지 패키지를 포함하고 있습니다:

  • core: 백엔드 일체형 서비스.
  • phrases: i18n 키 → 구문 자원.
  • schemas: 데이터베이스 및 공유된 타입스크립트 스키마.
  • ui: core와 상호 작용하는 웹 SPA.

풀스택을 위한 테크 스택

우리가 자바스크립트 생태계를 포용하고 타입스크립트를 우리의 주된 프로그래밍 언어로 사용하고 있기 때문에, 많은 선택들은 분명하다 (기반으로 나의 선호도 😊):

  • koajs는 백엔드 서비스 (core)를 위한 것입니다: 나는 express에서 async/await를 사용하는 데 무척 어려운 경험을 했으므로, 내가 기본 지원이 있는 것을 사용하기로 결정했습니다.
  • i18next/react-i18next는 i18n (phrases/ui)를 위한 것입니다: API의 단순함과 좋은 타입스크립트 지원을 좋아한다.
  • react는 SPA (ui)를 위한 것입니다: 그냥 개인적인 선호도입니다.

그런데 스키마는 어떨까요?

여기서 뭔가가 여전히 없습니다: 데이터베이스 시스템과 스키마 <-> 타입스크립트 정의 매핑.

General v.s. 견해

그 시점에서, 나는 두 가지 인기있는 접근방식을 시도했습니다:

  • 많은 장식자와 함께 ORM을 사용한다.
  • Knex.js와 같은 쿼리 빌더를 사용한다.

그러나 그들 둘 다 이전 개발 중에 이상한 느낌을 만들어냅니다:

  • ORM에 대해: 나는 장식자의 팬이 아니며, 데이터베이스의 다른 추상 레이어는 팀에게 더 많은 학습 노력과 불확실성을 초래합니다.
  • 쿼리 빌더에 대해: 제약 조건이 있는 SQL을 작성하는 것과 같습니다 (좋은 방향으로), 그러나 그것은 실제 SQL이 아닙니다. 그래서 우리는 많은 시나리오에서 원시 쿼리를 위해 .raw()를 사용해야 합니다.

그 후 이 글을 보았습니다: 'Knex.js를 그만하세요: SQL 쿼리 빌더 사용은 안티 패턴입니다'. 그 제목은 공격적으로 보이지만, 내용은 훌륭합니다. 이것은 강력하게 나에게 'SQL은 프로그래밍 언어입니다'라고 상기시키고, 나는 다른 계층을 추가하고 힘을 줄이는 대신에 원래의 언어와 데이터베이스 기능을 이용하기 위해 SQL을 직접 작성할 수 있다는 것을 깨달았습니다.

결국, 나는 Postgres와 Slonik (오픈 소스 Postgres 클라이언트)에 고수하기로 결정했습니다, 위의 글에 따르면:

...사용자가 다른 데이터베이스 방언들 사이에서 선택할 수 있는 이점은 보잘 것 없고, 한 번에 여러 데이터베이스를 개발하는 오버헤드는 상당합니다.

SQL <-> TypeScript

SQL을 쓰는 다른 장점은 우리가 TypeScript 정의의 단일 출처로 쉽게 사용할 수 있게 해줍니다. 나는 코드 생성기를 작성하여 SQL 스키마를 온전한 우리의 백엔드에서 사용할 TypeScript 코드로 변환하고, 결과는 나쁘지 않아 보입니다:

우리는 심지어 'jsonb'를 타입스크립트와 연결하고 백엔드 서비스에서 필요한 경우 유형 유효성 검사를 처리할 수도 있습니다.

결과

최종 의존성 구조는 이렇게 보입니다:

그것이 일방향 다이어그램이라는 것을 의심할 수 있는 분들도 계실 겁니다, 이것은 우리가 명확한 아키텍처를 유지하고 프로젝트가 성장함에 따라 확장 능력을 유지하도록 크게 도왔습니다. 게다가, 코드는 (기본적으로) 모두 TypeScript로 있습니다.

개발 체험

패키지와 설정 공유

내부 의존성

pnpmlerna는 내부 작업공간 의존성에서 훌륭한 일을 하고 있습니다. 우리는 프로젝트 루트에서 다음의 명령을 사용하여 동료 패키지를 추가합니다:

이것은 'logto/schemas'를 'logto/core'에 의존성으로 추가할 것입니다. 당신의 내부 의존성의 의미론적 버전을 유지하면서, pnpm는 또한 이것들을 pnpm-lock.yaml에서 올바르게 연결할 수 있습니다. 결과는 이렇게 보일 것입니다:

설정 공유

우리는 모노레포의 모든 패키지를 "독립적"으로 취급합니다. 따라서 우리는 설정 공유를 위한 표준 접근법을 사용할 수 있으며, 이는 'tsconfig', 'eslintConfig', 'prettier', 'stlyelint', 그리고 'jest-config'를 포함합니다. 이 프로젝트를 예로 들어 보세요.

코드, 린트, 그리고 커밋

나는 일상적인 개발을 위해 VSCode를 사용하고 있습니다, 그리고 짧게 말하면, 프로젝트가 제대로 설정되면 아무 것도 다르지 않습니다:

  • ESLint와 Stylelint는 정상적으로 작동합니다.
    • 만약 당신이 VSCode ESLint 플러그인을 사용하고 있다면, 패키지별 ESLint 설정을 따르게 하기 위해 아래의 VSCode 설정을 추가하세요 (자신의 'pattern'의 값을 바꾸세요):
  • husky, commitlint, 그리고 lint-staged는 예상대로 작동합니다.

컴파일러와 프록시

우리는 프론트엔드와 백엔드를 위해 서로 다른 컴파일러를 사용하고 있습니다: UI (React)를 위한 parceljs와 모든 다른 순수 타입스크립트 패키지를 위한 tsc. 당신이 아직 시도해 보지 않았다면 parceljs를 시도해보라고 강력히 추천합니다. 이것은 정말로 '제로 설정' 컴파일러로서, 다른 파일 유형을 우아하게 처리합니다.

Parcel은 자체 프론트엔드 개발 서버를 호스팅하고, 생산 출력은 단지 정적 파일입니다. 우리는 CORS 문제를 피하려면 같은 출신에서 API와 SPA를 마운트하고 싶기 때문에, 아래의 전략이 작동합니다:

  • 개발 환경에서, 간단한 HTTP 프록시를 사용하여 트래픽을 Parcel 개발 서버로 리다이렉션합니다.
  • 생산에서, 바로 정적 파일을 제공합니다.

당신은 프론트엔드 미들웨어 함수 구현을 여기에서 찾을 수 있습니다.

감시 모드

우리는 각 패키지에 대한 'package.json'에 파일 변경사항을 감시하고 필요하면 재컴파일하는 'dev' 스크립트를 가지고 있습니다. lerna 덕분에 'lerna exec'를 사용하여 패키지 스크립트를 병렬로 실행하는 것은 쉽습니다. 루트 스크립트는 이렇게 보일 것입니다:

요약

이상적으로, 새로운 엔지니어/기여자가 시작하는 데에는 두 단계만 필요합니다:

  1. 레포를 클론합니다
  2. pnpm i && pnpm dev를 합니다

마무리 노트

우리 팀은 이 접근법을 통해 일년 동안 개발하였고, 우리는 그것에 매우 만족하고 있습니다. 우리의 GitHub 레포를 방문하여 프로젝트의 최신 모양을 확인하세요. 요약하자면:

고통

  • JS/TS 생태계에 익숙해져야 합니다
  • 올바른 패키지 매니저를 선택해야 합니다
  • 몇 가지 추가적인 한 번 설정이 필요합니다

이익

  • 전체 프로젝트를 한 대여소에서 개발하고 유지합니다
  • 개발 스킬 요구 사항을 단순화합니다
  • 코드 스타일, 스키마, 구문, 유틸리티 공유
  • 향상된 의사소통 효율성
    • API 정의가 무엇입니까? 같은 질문이 더 이상 없습니다
    • 모든 엔지니어들은 같은 프로그래밍 언어로 이야기하고 있습니다
  • CI/CD를 쉽게합니다
    • 빌드, 테스트, 발행을 위해 같은 툴체인을 사용합니다

이 글은 몇 가지 주제를 다루지 않았습니다: 레포를 처음부터 설정하기, 새 패키지 추가하기, GitHub 액션을 CI/CD에 활용하기 등등. 만약 나가 각각를 확장한다면 이 글은 너무 길어질 것입니다. 나중에 어떤 주제를 보고 싶은지 알려주시기 바랍니다.