반응형

사내에서 프로젝트 별 코드의 파편화를 줄이고 디자인 시스템을 만들려고 하고 있다.

인수인계 겸 정리한 문서를 공개해도 상관 없을꺼 같아서 블로그에 간단 정리해본다.

 

사용 툴

  • 🏎 Turborepo — 모노레포 라이브러리로 여러 패키지나 프로젝트 간의 의존성 관리를 효율화하고 Monorepo 내에서 빌드 프로세스를 간소화
  • 🚀 React — 프론트엔드 사용 라이브러리
  • 🛠 Parcel — 번들러로 설정 없이 간편하게 쓸 수 있어서 사용, 추후 rollup으로 변경될 가능성도 있음
  • 📖 Storybook — UI 컴포넌트 문서화 및 공유 & 협업용
  • TypeScript는 정적 타입 검사를 위한 도구
  • ESLint는 코드 린팅을 위한 도구
  • Prettier는 코드 포맷팅을 위한 도구
  • Changesets는 버전 관리와 변경 로그를 관리하는 도구
  •  

라이브러리를 구현할때는 radix-ui를 사용합니다.

자체 구현은 최대한 지양합니다. (개발자 나가면 유지보수가 안됨 ㅠㅠ)

 

커밋 컨벤션

build: 빌드 시스템이나 외부 의존성에 관련된 변경사항 (예: gulp, broccoli, npm의 패키지 변경 등)
chore: 기타 변경사항 (코드 수정 없이 설정을 변경하는 경우)
ci: 지속적 통합(CI) 설정과 스크립트 변경사항 (예: Travis, Circle, BrowserStack, SauceLabs의 설정 변경 등)
docs: 문서에만 영향을 미치는 변경사항
feat: 새로운 기능에 대한 커밋
fix: 버그 수정에 대한 커밋
perf: 성능을 개선하는 코드 변경사항
refactor: 버그를 수정하거나 기능을 추가하지 않는 코드 변경사항
revert: 이전 커밋을 되돌리는 작업
style: 코드 의미에 영향을 주지 않는 변경사항 (공백, 포맷팅, 누락된 세미콜론 등)
test: 테스트 추가, 리팩토링 테스트 코드, 누락된 테스트 추가 등
echo "foo: some message" # fails
echo "fix: some message" # passes

 

conventional commit을 사용합니다. 허용되지 않은 양식은 husky와 commitlint library가 막습니다.

https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional

 

모노 레포 구성

 

모노레포는 여러 개의 프로젝트나 라이브러리를 단일 저장소에서 관리하는 방식을 말합니다. 이러한 접근 방식은 큰 프로젝트나 여러 팀이 협업할 때 많은 이점을 제공합니다. 주요 이점으로는 코드의 재사용성 증가, 의존성 관리의 단순화, 통합된 버전 관리 등이 있습니다.

turbo repo를 이용한 모노 레포 구조를 가집니다. 각 폴더는 하나의 레포지토리를 구성합니다.

 

- apps
    ㄴ docs -> 문서용 storybook
    ㄴ RadixComponents -> 사용하는 공통 컴포넌트 (현재는 사용하고 있지 않음)
    ㄴ TTTable  -> 테이블 구현체

- packages
    ㄴ shared  -> 공통 변수
    ㄴ @TDS/utils -> 공통 함수
    ㄴ designFiles -> designTokens, icons들 저장소

- configs
    ㄴ eslint -> eslint 설정
    ㄴ tsconifg -> typescript 설정
    ㄴ jestconfig -> jest 설정
  • apps: 실제 사용자에게 제공되는 애플리케이션들이 위치합니다. 여기에는 문서를 위한 Storybook(docs), 공통 컴포넌트(RadixComponents), 테이블 구현체(TTTable) 등이 포함될 수 있습니다.
  • packages: 재사용 가능한 코드, 공통 함수, 디자인 토큰, 아이콘 등을 포함하는 패키지들이 위치합니다. 이러한 패키지들은 apps 내의 프로젝트들 및 배포 후 자사 프로젝트에 의해 사용될 수 있습니다.
  • configs: 프로젝트 전체에 걸쳐 공유되는 설정 파일들이 위치합니다. ESLint, TypeScript, Jest 등의 설정이 포함될 수 있습니다.

script 실행 방법

최상위 package.json에 정의된 스크립트는 모노레포 내의 여러 프로젝트나 패키지에 대한 작업을 조정합니다. 예를 들어, 다음과 같은 스크립트가 있을 수 있습니다:

  • build: 애플리케이션과 패키지를 빌드합니다.
  • test: 애플리케이션과 패키지의 테스트를 실행합니다.
  • lint: 코드 스타일과 문법 검사를 전체 프로젝트에 걸쳐 실행합니다.
  • start: 개발 모드에서 특정 애플리케이션을 실행합니다.

최상위 폴더에서 pnpm run build시 하위 레포지토리의 build 명령어가 있는 레포지토리는 병렬적으로 해당 명령어를 전부 실행합니다.

다른 자세한 기능들은 turbo repo docs를 참고하시기 바랍니다.

레포지토리 배포 프로세스

1. pnpm run changeset
2. 패치 버젼, summary 등 작성
3. pnpm run changeset version (version up)
4. commit
5. PR 및 리뷰
6. master branch에 반영
7. gitlab runner에서 자동 배포

1. pnpm run changeset

  • 이 명령은 변경 사항을 추가하기 위해 Changesets CLI를 실행합니다.

2. 패치 버전, 요약 등 작성

  • 선택한 패키지와 업데이트 유형(예: 패치)에 대한 설명을 작성합니다. 이 설명은 추후 changelog에 반영됩니다.
  • 이 과정은 변경 사항에 대한 명확한 기록을 남기며, 팀원들이나 사용자들이 어떤 변경 사항이 있었는지 쉽게 파악할 수 있도록 합니다.

3. pnpm run changeset version (버전 업)

  • 이 명령은 Changesets가 생성한 변경 사항 파일을 기반으로 패키지의 버전을 실제로 업데이트하고, 각 패키지의 changelog를 업데이트합니다.
  • 변경 사항에 대한 모든 정보는 패키지별로 관리되며, 이 단계에서 자동으로 패키지 버전이 업데이트됩니다.

4. 커밋

  • 버전 업데이트와 changelog 변경 사항을 Git 저장소에 커밋합니다. 이 커밋에는 버전이 업데이트된 패키지와 변경된 changelog 내용이 포함됩니다.

5. PR(풀 리퀘스트) 및 리뷰

  • 변경 사항을 메인 브랜치(예: master)에 병합하기 전에, 풀 리퀘스트를 생성합니다. 이는 코드 리뷰 과정을 통해 변경 사항을 검토하고 팀 내에서 합의를 이루기 위함입니다.
  • 팀원들이 변경 사항을 리뷰하고, 필요한 피드백을 제공합니다. 리뷰 과정을 통해 코드 품질을 유지하고, 오류를 사전에 방지할 수 있습니다.

6. master 브랜치에 반영

  • 리뷰를 통과한 후, 풀 리퀘스트를 메인 브랜치에 병합합니다. 이는 변경 사항이 공식적으로 프로젝트에 반영되었음을 의미합니다.

7. GitLab Runner에서 자동 배포

  • 메인 브랜치에 병합된 후, GitLab CI/CD 파이프라인이 실행됩니다. GitLab Runner는 이 파이프라인을 통해 정의된 배포 작업을 자동으로 수행합니다.
  • 배포 과정은 테스트, 빌드, 배포 단계를 포함할 수 있으며, 이 모든 과정은 자동으로 실행됩니다. 성공적으로 배포가 완료되면, 변경된 사항이 실제 환경에 적용됩니다.

** package.json에 명시한 build 결과물 및 publish config의 exports 파일만 배포하므로 꼼꼼히 확인해주세요.

private(사내망) 배포 방법

 

1. 최상위 .npmrc 에 registry를 설정합니다. (구현되어 있음)

@TDS:registry=http://#######/api/v4/projects/311/packages/npm/

위 설정의 뜻은 @TDS 라는 이름을 앞에 붙인 라이브러리를 사내에(private registry) 배포 및 사용하겠단 뜻입니다.

사용하는 프로젝트의 .npmrc에도 동일하게 작성합니다

 

2. 배포하고 싶은 라이브러리의 package.json을 설정합니다. (중요!)

{
  "name": "@TDS/radix_components",
  "version": "0.0.0",
  "sideEffects": ["./src/index.scss"],
  "license": "MIT",
  "exports": {
    ".": {
      "require": "./src/index.tsx",
      "import": "./src/index.tsx"
    },
    "./index.scss": "./src/index.scss",
    "./package.json": "./package.json"
  },
  "source": "src/index.tsx",
  "main": "dist/index.js",
  "files": [
    "dist/**"
  ],
  "scripts": {
    "test": "jest --passWithNoTests",
    "build": "tsup src/index.tsx --format esm,cjs --dts --external react",
    "parcel:build": "parcel build",
       .... 
  },
  "devDependencies": {
...
  },
  "dependencies": {
....
  },
  "resolutions": {
   ....
  },
  "publishConfig": {
    "access": "restricted",
    "registry": "http://####/api/v4/projects/311/packages/npm/",
    "exports": {
      ".": {
        "require": "./dist/index.js",
        "import": "./dist/index.mjs",
        "types": "./dist/index.d.ts"
      },
      "./package.json": "./package.json",
      "./index.css": "./dist/index.css"
    },
    "import": "./dist/index.mjs",
    "main": "./dist/index.js",
    "module": "./dist/index.mjs",
    "types": "./dist/index.d.ts"
  }
}

 

여기서 중요하게 볼 것은 exports와 publish config, private, sideEffects, name, version 입니다.

name

  • 라이브러리의 고유한 이름을 설정합니다. 여기서는 @TDS/radix_components로, @TDS는 범위(scope)를 나타내며, 같은 범위 내에서 유니크한 이름을 가지게 합니다. npm 등의 패키지 매니저를 통해 이 이름으로 라이브러리를 찾고 설치할 수 있습니다.

version

  • 라이브러리의 현재 버전을 나타냅니다. 세마틱 버저닝(semantic versioning) 원칙에 따라 major.minor.patch 형식으로 관리됩니다. 초기 상태인 0.0.0에서 시작하여, 라이브러리가 업데이트될 때마다 적절한 버전 업을 합니다.

sideEffects

  • 패키지가 부수 효과(side effects)를 가지는지 여부를 웹팩(Webpack) 같은 모듈 번들러에 알립니다. 여기서는 ./src/index.scss 파일이 부수 효과를 가진다고 명시되어 있어, 이 파일을 제외하고 트리 쉐이킹(tree shaking)을 적용할 수 있습니다.

exports

  • 패키지의 내보내기(entry points)를 정의합니다. 이는 패키지를 사용하는 소비자가 접근할 수 있는 모듈의 경로를 명시합니다. 예를 들어, require 또는 import를 통해 메인 모듈을 가져오거나, 추가적인 파일(index.scss)에 대한 접근 방법을 제공합니다.
  • 개발자 경험 향상을 위해서 모노레포 내에서는 코드를 참조합니다. 해당 방식을 사용하지 않으면 merge 및 테스트때마다 강제로 빌드를 한번씩 실행해줘야 합니다.

publishConfig

  • 패키지를 배포할 때 사용할 설정을 정의합니다. access 필드로 접근성을 설정하며, registry 필드는 패키지가 배포될 npm 레지스트리의 URL을 지정합니다. 또한, exports 섹션은 배포된 패키지의 내보내기 설정을 다시 정의하여, 사용자가 패키지의 빌드된(dist) 버전을 사용할 수 있게 합니다.
  • 개발자 경험을 향상하기 위해서 모노레포 빌드시 해당 옵션을 사용하도록 합니다.

private

  • (이 설정은 예시에 명시되어 있지 않지만 중요합니다) 이 필드가 true로 설정되면, 패키지가 비공개로 설정되어 npm 등의 공개 레지스트리에 배포되지 않습니다. 주로 개인 프로젝트나 팀 내부에서 사용할 패키지에 적용됩니다.
  • configs 레포지토리들은 private로 적용합니다.

 

gitlab의 package registry에서 현재 배포된 파일들을 확인 가능합니다

 

사내에 배포시 gitlab의 package registry에서 현재 배포된 파일들을 확인 가능합니다.

라이브러리 사용 방법

  "dependencies": {
    "@DB3/designFiles": "^0.0.0",
    "@DB3/tttable": "1.0.2",
       ...
   }

“내부망에 접속된 컴퓨터”에서 package.json에 명시해서 사용하면 됩니다.

 

CSS 관리법 - Design Variable

Figma Variable은 Figma 디자인 도구에서 사용되는 기능으로, 사용자가 디자인 내에서 텍스트나 색상과

같은 속성을 변수로 설정하여 재사용할 수 있게 해주는 기능입니다.

 

design variable에 대한 간단한 설명

 

개발의 "variable(변수)"처럼 dark mode, theme 등 여러 상황에 알맞게 값을 바꿔주는 기능
color, text 등 여러 값을 유연하게 변경 가능

반영 방법

enterprise 계정에서만 figma web api를 통한 auto import 가능 (우리 회사는 해당 없음)

그래서, figma plugin을 활용해서 design variable은 variable2CSS plugin으로 받아오고

typography 관련은 global style to code plugin으로 수동으로 받아오는 식으로 구현해야합니다.

피그마 기준으로 plugin을 통해 컬러를 뽑아내어 사용합니다.

절대로 임의의 컬러를 추가해서 사용하면 안됩니다.

해당 파일은 packages/designFiles 라이브러리 내부에 구현하고, 공유해서 사용합니다.

ex)

[data-theme="light"] {
  /* colors */
  --adaptive-blanket-layout-split: var(--adaptive-grey-opacity-grey-opacity300, #001d3a2e);
  --adaptive-blanket-modal-background: var(--adaptive-grey-opacity-grey-opacity500, #03183275);
  --adaptive-blue-blue100: #cae8ff;
  --adaptive-blue-blue1000: #004491;
  --adaptive-blue-blue1100: #003571;
  ....(생략)
  }

  [data-theme="dark"] {
  /* colors */
  --adaptive-blanket-layout-split: var(--adaptive-grey-opacity-grey-opacity300, #001d3a2e);
  --adaptive-blanket-modal-background: #13151799;
  --adaptive-blue-blue100: #00326a;
  --adaptive-blue-blue1000: #98cefd;
  --adaptive-blue-blue1100: #b3defe;
  ...(생략)
  }

darkMode, lightMode 두가지 테마를 variable을 토글되게 해서 사용하고, semantic한 네임을 사용하도록 ux연구원분들께 요청합니다.

어떻게 모드를 바꾸는지는 designFiles/setDarkMode.ts 코드를 참고합니다.

다크모드를 적용할 생각이 없어도 해당 방식으로 구현해야 편할껍니다...

*프로젝트에서 실제로 어떻게 사용할껀지는 논의가 필요함.

가장 쉬운 방법은 import ‘designFiles/index.css’ 이다.

테스트(JEST) 사용법

예시 config 파일

const { dirname, join } = require('path')
const path = require('path')

const jestConfig = require('jest-config/jest.config.js')

const customJestConfig = {
  ...jestConfig,

  testEnvironment: 'jsdom',

  moduleNameMapper: {
    ...jestConfig.moduleNameMapper,

    ...{ '@table/(.*)$': `${__dirname}/../@TDS-TTTable/src/$1` },
  },
}

module.exports = customJestConfig

jest-config 레포지토리에서 jest.config.js와 setupTests.ts를 가져옵니다.

describe('Circle class', function() {
  describe('when area is calculated', function() {
    it('it should sets the radius', function() { ... });
    it('it should sets the diameter', function() { ... });
    it('it should sets the circumference', function() { ... });
  });
});

 

테스트는 describe-it pattern 으로 가독성 있게 작성합니다.

https://woonjangahn.gitbook.io/logs/typescript/testing/jest-describe-it

내부는 when then given pattern에 맞게 작성합니다.

https://brunch.co.kr/@springboot/292

프론트엔드 특성상 스타일과 사용 라이브러리가 자주 바뀔 일이 많습니다.

스타일은 테스트 하지 말고 로직(hook)과 컴포넌트 단위로 테스트를 작성합니다.

코드 한줄 한줄을 검증하는 화이트박스가 아니라

블랙박스 테스트, 유저의 동작을 end-to-end로 검증하는 테스트 위주로 작성합니다.

유용한 명령어들

최상위 package.json 참고

  • pnpm build - Storybook 를 포함한 모든 패키지 빌드
  • pnpm dev - 모든 패키지를 로컬에서 실행하고 Storybook으로 미리보기
  • pnpm lint - 모든 패키지에 대해 lint 진행
  • pnpm changeset - changeset 생성
  • pnpm clean - 모든 node_modulesdist 폴더 정리 (각 패키지의 clean 스크립트 실행)
반응형

+ Recent posts