토스 라이브러리 구조 들여다보기

윤다은·2022년 12월 15일
0

💡 2022.12.14일 main 브랜치를 기준으로 작성되었습니다.

세팅은 어떻게 되어 있을까?

1. 모노레포

토스의 라이브러리는 모노레포로 구성되어 있습니다. 모노레포 세팅을 살펴보겠습니다.

1-1. yarn berry 이용

.yarnc 파일을 보니 얀베리를 사용하는 것을 알 수 있습니다.

yarn-path: .yarn/releases/yarn-3.2.3.cjs

1-2. plug n play 사용

.pnp.cjs이 존재하는 것으로 플러그 앤 플레이를 하고 있으며 더하여 .yarn/cache이 있는 것으로 보아 제로 인스톨 전략을 쓰는 것을 알 수 있습니다.

1-3. lerna 사용

.lerna.json : 여러 패키지 관리를 도와주는 Lerna를 사용하고 있습니다.

1-4. workspace 분리

크게 3가지로 분리하고 있습니다.
1. docs : 문서
2. packages : 패키지들 Node환경에서 사용할 수 있는 common과 리액트에서 사용하는 react로 나뉘어 있습니다.
3. configs : 설정

2. CI

2-1. CircleCI 사용

.circleci : 소스코드를 통합하고 빌드, 배포를 자동화하는 파이프라인 구성 툴입니다.

2-2. .circleCI 파헤치기

토스에선 circleCI를 통해 린팅, 테스트, 타입 체크 등을 병렬적으로 수행하고 있습니다. 자세한 설정은 아래를 참고해주세요.

# circleCI를 사용하려는 버전을 나타냅니다.
version: 2.1

# 이 파일 내에서 사용할 수 있는 파라미터를 나타냅니다. 
# 여기선 pipeline.parameters.pull_request를 선언하였으면 기본값은 false입니다. 
parameters:
  pull_request:
    type: boolean
    default: false
    
# 일종의 alias라고 보면 됩니다. 명령들을 재사용 할 수 있도록 모아둔 패키지입니다.
orbs:
  slack: circleci/slack@4.5.0

# job안에서 진행될 명령들을 모아두는 곳입니다. 하나의 커맨드를 여러 job에서 사용할 수 있습니다.
commands:
  # setup이란 커맨드
  setup:
    # 여러 개의 step으로 구성
    steps:
      # 아래를 실행하세요 
      - run:
          # circle CI UI내에서 보일 step의 이름을 의미합니다.
          name: Install yarn
          # 쉘을 통해 수행될 코맨드
          command: |
            COREPACK_PATH=$HOME/.local/bin

            mkdir -p $COREPACK_PATH
            eval "$(echo PATH=$COREPACK_PATH:\$PATH | tee -a $BASH_ENV)"

            corepack enable --install-directory $COREPACK_PATH
            yarn install --immutable --immutable-cache
  export_published_version:
    description: Export Published Version Environment Variable
    steps:
      - run:
          name: Export PUBLISHED_VERSION
          command: echo "export PUBLISHED_VERSION=$(node -p "require('./lerna.json').version")" >> $BASH_ENV
  export_commit_message:
    description: Export Commit Message
    steps:
      - run:
          name: Export COMMIT_MESSAGE
          command: echo "export COMMIT_MESSAGE=\"$(printf "%s\n" "$(git log --format=%B -n 1 "$CIRCLE_SHA1" | head -n 1)")\"" >> $BASH_ENV

# job이란 step들의 모음을 의미하며 Job의 모든 step들은 하나의 머신에 동작합니다.
# 여기서 머신은 docker container가 될 수도 있고 실제 물리적 머신이 될 수도 있습니다.
# 같은 job끼리는 캐싱이 동작합니다. 
jobs:
  lint:
    docker:
      - image: cimg/node:16.17
    steps:
      - checkout
      - setup
      - run:
          name: Lint
          command: yarn eslint -c .eslintrc.js $(git diff --name-only --diff-filter=ACMRUXB origin/main | grep -E "(.js$|.ts$|.tsx$)")
  typecheck:
    parallelism: 2
    docker:
      - image: cimg/node:16.17
    steps:
      - checkout
      - setup
      - run:
          name: Typecheck
          command: |
            WORKSPACES_TO_TEST=$(
              yarn workspaces since list origin/main HEAD |
                circleci tests split
            )
            echo $WORKSPACES_TO_TEST

            INCLUDE=$(
              echo $WORKSPACES_TO_TEST |
                # 띄어쓰기로 연결된 문자열을 콤마로 연결
                sed 's/ /,/g'
            )
            yarn workspaces since run "typecheck" remotes/origin/main --include="{$INCLUDE,}"

  pre-pack:
    parallelism: 2
    docker:
      - image: cimg/node:16.17
    steps:
      - checkout
      - setup
      - run:
          name: Prepack
          command: |
            WORKSPACES_TO_TEST=$(
              yarn workspaces since list origin/main HEAD |
                circleci tests split
            )
            echo $WORKSPACES_TO_TEST

            INCLUDE=$(
              echo $WORKSPACES_TO_TEST |
                # 띄어쓰기로 연결된 문자열을 콤마로 연결
                sed 's/ /,/g'
            )
            yarn workspaces since run "prepack" remotes/origin/main --include="{$INCLUDE,}"

  check-peer:
    docker:
      - image: cimg/node:16.17
    steps:
      - checkout
      - setup
      - run:
          name: Check Peer Dependency
          command: ./.scripts/check-peer.sh || (echo "Peer Dependency 오류가 발생했습니다."; exit -1)

  test:
    docker:
      - image: cimg/node:16.17
    parallelism: 2
    steps:
      - checkout
      - setup
      - run:
          name: Jest
          command: |
            UPDATED_PACKAGES=$(yarn workspaces since list origin/main HEAD)
            echo "updated packages:"
            echo $UPDATED_PACKAGES

            UPDATED_DIRS=$(echo $UPDATED_PACKAGES | tr ' ' ',')

            if [[ -z $UPDATED_DIRS ]]; then
              echo "업데이트된 패키지가 없습니다."
              exit 0
            fi

            # 테스트 파일이 없는 경우 아래 오류 발생
            # panic: runtime error: index out of range [0] with length 0
            if [[ -z $(circleci tests glob "{$UPDATED_DIRS}/**/*.{test,spec}.{ts,tsx}") ]]; then
              echo "업데이트된 패키지에 실행할 테스트 파일이 없습니다."
              exit 0
            fi

            TESTS=$(
              circleci tests glob "{$UPDATED_DIRS}/**/*.{test,spec}.{ts,tsx}" | \
                awk '!/__manual__/' | \
                  circleci tests split --split-by=timings
            )

            echo "tests to run:"
            echo $TESTS | xargs -n 1 echo

            if [[ -z $TESTS ]]; then
              echo "실행할 테스트가 없습니다."
              exit 0
            fi

            yarn jest $TESTS --passWithNoTests --runInBand
          environment:
            YARN_ENABLE_IMMUTABLE_INSTALLS: 'false'
            JEST_JUNIT_OUTPUT_DIR: ./.test-reports/junit/
      - store_test_results:
          path: ./.test-reports/junit/
      - store_artifacts:
          path: ./.test-reports/junit

  test-all:
    docker:
      - image: cimg/node:16.17
    parallelism: 4
    steps:
      - checkout
      - setup
      - run:
          name: Jest
          command: |
            yarn jest --runInBand
          environment:
            JEST_JUNIT_OUTPUT_DIR: ./.test-reports/junit/
      - store_test_results:
          path: ./.test-reports/junit/
      - store_artifacts:
          path: ./.test-reports/junit

  save-git-cache:
    docker:
      - image: cimg/node:16.17
    steps:
      - checkout
      - setup
      - save_checkout_cache

# 워크플로우에선 여러 job들의 집합과 job들간의 스케줄링을 이야기합니다.
workflows:
  main:
    jobs:
      #워크 플로우에서 테스트, 린트오 같은 작업들은 비교적 비용이 많이 소모되지 않으므로 job들의 앞단에 위치하는 것이 좋습니다. 
      # 아래에 있는 jobe들이 parallel하게 동작합니다. 
      # 브랜치 상황에 따라 main을 제외하고 테스트하는 경우가 있고 main만 테스트 하는 경우가 있습니다. 
      - test:
          filters:
            branches:
              ignore: main
      - test-all:
          context:
            - npm-public
          filters:
            branches:
              only: main
      - lint:
          filters:
            branches:
              ignore: main
      - typecheck:
          filters:
            branches:
              ignore: main
      - pre-pack:
          filters:
            branches:
              ignore: main
      - check-peer:
          filters:
            branches:
              ignore: main

3. tsconfig

tsconfig를 목적에 따라 다르게 사용합니다.

  1. 빌드를 위한 tsconfig
    빌드 시엔 테스트 코드에 대한 타입 검사를 진행하지 않습니다.
//tsconfig.build.json
{
  "extends": "./tsconfig.json",
  "exclude": ["**/jest.setup.ts", "**/*.test.*", "**/*.spec.*", "**/*.stories.*", "**/__storybook__/*"]
}
  1. 신버전 자바스크립트 사용을 위한 설정
//tsconfig.esm.json
{
  "extends": "./tsconfig.build.json",
  "compilerOptions": {
    "module": "esnext", //신버전 자바스크립트 사용 es2015등으로도 지정 가능
  }
}
  1. eslint 사용을 위한 설정
//tsconfig.eslint.json
{
  "extends": "./tsconfig.json",
  "include": ["./.eslintrc.js"]
}

4. 의존성

토스의 경우 크게 3가지의 패키지로 이루어져있습니다.
1. docs
2. packages
3. cofigs
이 중 configs는 rollup에 관한 설정이 있습니다. 이 rollup설정은 대부분의 패키지에서 참조하고 있습니다.
packages 는 노드 환경에서 쓸 수 있는 commons 와 리액트 환경에서 쓸 수 있는 react로 나뉘어져 있습니다.

자세한 의존성이 궁금하신 분들은 아래 이미지를 참고해주세요

토스 패키지 의존성 이미지로 확인하기 점선으로 둘러싸인 사각형은 참조되는 패키지를 의미합니다. 전체적으로 설정 패키지가 대부분에 패키지에 의해 참조되는 모습을 볼 수 있습니다. 토스 패키지 의존성 이미지

소감

토스 레포를 살펴보면서 인상 깊었던 점입니다.
1. 이미 있는 라이브러리(ex. react-query, recoil)를 한번 더 래핑해서 패키지로 사용하는 점
2. 다른 라이브러리 중엔 리액트 18을 지원하는 버전이 나오면서 리액트 17에 대한 버전을 더 이상 지원하지 않는 경우도 있었는데 토스의 경우 16-18버전을 지원한다는 점이 신기했습니다.
3. 변환하는 자바스크립트 버전을 esnext로 한 점이 인상깊었습니다. 언제나 새로운 점을 도전해보는 것 같습니다.

profile
코끼리가 코로 걸어다니는 코드를 지양합니다.

0개의 댓글