Croot Blog

Home About Tech Hobby Archive

공통 컴포넌트 패키지 배포

서론

최근 AI 교과서 제작 프로젝트 중간 즈음에 퍼블 + 일부 프론트 개발 담당으로 참여하게 되었습니다. 이미 상당 부분 진행되었지만, 소스코드의 스타일 버그 픽스와 UDL(Unified Design Language) 적용, 디자인 및 컨셉 변경 등 추가적인 수정이 필요한 상황이었습니다.

문제점

프로젝트를 진행하면서 다음과 같은 주요 문제점을 파악했습니다.

  1. 개발 표준 및 프로세스 부재: 여러 업체가 각각의 프로젝트를 진행하면서 개발 표준과 프로세스에 대한 합의가 충분히 이루어지지 않았습니다.
  2. 통합되지 않은 UI 개발: 공통된 기획 컨셉과 디자인이 있었지만, 실제 UI 개발에서는 이들이 통합되지 않았습니다.
  3. UDL 지원 미흡: 일부 프로젝트에 적용된 UI 프레임워크에서는 UDL 지원이 미흡하여 일관된 사용자 경험을 제공하는 데 어려움이 있었습니다.
  4. 반복적인 버그 수정: 동일한 이슈에 대해 각 프로젝트별로 개별적으로 버그 수정을 해야 했습니다.

이러한 문제를 해결하고 UDL을 효과적으로 적용하기 위해, 공통된 UI 컴포넌트를 제공할 필요성을 느꼈습니다.

해결방안

이번 AI 교과서 제작 프로젝트에서 발생한 문제들을 해결하기 위해 다음과 같은 방안을 생각해보았습니다.

1. 공통 UI 컴포넌트를 패키지로 구성하여 각 프로젝트에 의존 주입

프로젝트마다 UI 컴포넌트를 개별적으로 개발하는 대신, 공통 UI 컴포넌트를 패키지로 구성합니다. 이 패키지를 각 프로젝트에 의존성 주입하여 사용함으로써, 중복 작업 최소화와 일관된 UI를 유지하고 개발 효율성을 높입니다.

2. 각 시스템 상황에 맞게 유연한 스타일링 지원

모든 프로젝트가 동일한 스타일을 강제하는 대신, 각 시스템의 특성과 요구에 맞게 유연한 스타일링을 지원합니다. 이를 위해 테마 설정 및 스타일 오버라이드를 통해 각 프로젝트가 필요에 따라 스타일을 커스터마이징할 수 있도록 합니다.

3. UI와 개발 로직의 관심 분리

UI와 개발 로직을 명확히 분리하여 유지보수성과 재사용성을 높입니다. 컴포넌트 기반 개발을 통해 UI와 비즈니스 로직을 독립적으로 관리할 수 있게 하며, 이를 통해 코드의 가독성과 관리 용이성을 개선합니다.

준비

구성안

대략적인 서비스 구성안은 아래와 같습니다.

![0](/assets/img/2024-05-22-공통-컴포넌트-패키지-배포.md/0.png)_Untitled.png_

각 과목별 플랫폼 프로젝트에서는 RMS에 등록된 컨텐츠를 CDN을 통해 불러옵니다.

공통 UI 패키지를 새로 제작하고, 이를 각 플랫폼과 컨텐츠 프로젝트의 의존 모듈로 연결하여 통합된 UI/UX를 제공할 계획입니다.

Scope 패키지

pnpm workspace를 이용하여 작성

scope 패키지 기술스택

  • Package Manager : pnpm workspace

scope 패키지 설정

package.json ```json { "name": "ui-component", "private": true, "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "engines": { "node": "^18.19.0", "pnpm": "^8.9.2" }, "packageManager": "pnpm@8.9.2", "workspaces": [ "packages/*" ], "keywords": [], "author": "", "license": "ISC", "devDependencies": { "pnpm": "^8.9.2" } } ```
pnpm-workspace.yaml ```yaml packages: - 'packages/*' ```

그 외 기타 Lint 설정(Prettier, Eslint, Stylelint) 추가 하였다.

공통 UI 컴포넌트 패키지

공통 컴포넌트 패키지에서는 select, datepicker 등 atomic 한 UI 컴포넌트들을 제공하게 됩니다.

공통 컴포넌트 기술스택

  • Build Tool : Vite
  • Superset : Typescript
  • UI F/W : Vue3
  • UI Tool : Storybook

공통 컴포넌트 설정

  1. 프로젝트 생성 및 storybook 설치

     mkdir packages
     cd packages
     pnpm create vite
     # Project name: … elements
     # ✔ Select a framework: › Vue
     # ✔ Select a variant: › TypeScript
     pnpm dlx storybook@latest init
     # Packages: +605
     # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     # Progress: resolved 627, reused 605, downloaded 0, added 605, done
     # ╭──────────────────────────────────────────────────────╮
     # │                                                      │
     # │   Adding Storybook version 8.1.3 to your project..   │
     # │                                                      │
     # ╰──────────────────────────────────────────────────────╯
     #  • Detecting project type. ✓
     # Installing dependencies...
     # 
     # Scope: all 2 workspace projects
     # ../..                                    | -732 ---------------------------------------------------------
     # ../..                                    | Progress: resolved 86, reused 49, downloaded 0, added 0, done
     # Done in 2.3s
     #  • Adding Storybook support to your "Vue 3" app • Detected Vite project. Setting builder to Vite. ✓
     # 
     #   ✔ Getting the correct version of 9 packages
     #   ✔ Installing Storybook dependencies
     # . ✓
     # Installing dependencies...
     # 
     # Scope: all 2 workspace projects
     # Done in 2.7s
    
  2. package.json 설정

     {
         "name": "@ui-component/elements",
         "private": true,
         "version": "1.0.0",
         "type": "module",
         "scripts": {
             "dev": "vite",
             "build": "rm -rf ./dist && vue-tsc && vite build",
             "preview": "vite preview",
             "storybook": "storybook dev -p 6006",
             "build-storybook": "storybook build"
         },
         "files": [
             "dist"
         ],
         "types": "./dist/index.d.ts",
         "main": "./dist/elements.cjs.js",
         "exports": {
             ".": {
                 "types": "./dist/index.d.ts",
                 "import": "./dist/elements.es.js",
                 "require": "./dist/elements.cjs.js"
             },
             // 스타일도 배포
             "./dist/style.css": {
                 "import": "./dist/style.css",
                 "require": "./dist/style.css"
             }
         },
         //...생략
     }
    	
    
  3. entry 파일 작성

     // main.ts
    	
     import ButtonComponent from './components/Button/Button.vue';
     import HeaderComponent from './components/Header/Header.vue';
     import PageComponent from './components/Page/Page.vue';
    	
     export const Button = ButtonComponent;
     export const Header = HeaderComponent;
     export const Page = PageComponent;
    	
    
  4. vite 빌드 설정

     // vite.config.ts
    	
     //...생략
     plugins: [
         dts({
             exclude: ['**/*.stories.ts'],
             insertTypesEntry: true,
             copyDtsFiles: true
         }),
         vue()
     ],
     build: {
         lib: {
                 // 위에 설정해준 파일경로를 적어주어야 한다.
             entry: path.resolve(__dirname, 'src/main.ts'),
             name: 'elements',
             formats: ['es', 'cjs'],
             fileName: format => `elements.${format}.js`
         }
     }
     //...생략
    

빌드 결과

  1. 빌드 스크립트 실행.

     pnpm run build
     # > rm -rf ./dist && vue-tsc && vite build
     # 
     # vite v5.2.11 building for production...
     # ✓ 16 modules transformed.
     # 
     # [vite:dts] Start generate declaration files...
     # dist/style.css        1.97 kB │ gzip:  0.66 kB
     # dist/elements.es.js  56.98 kB │ gzip: 18.65 kB
     # [vite:dts] Declaration files built in 3069ms.
     # 
     # dist/style.css         1.97 kB │ gzip:  0.66 kB
     # dist/elements.cjs.js  39.57 kB │ gzip: 15.42 kB
     # ✓ built in 3.85s
    
  2. 빌드 산출물 확인

     dist
      ┣ components
      ┃ ┣ Button
      ┃ ┃ ┗ Button.vue.d.ts
      ┃ ┣ Header
      ┃ ┃ ┗ Header.vue.d.ts
      ┃ ┗ Page
      ┃ ┃ ┗ Page.vue.d.ts
      ┣ elements.cjs.js
      ┣ elements.es.js
      ┣ index.d.ts
      ┣ main.d.ts
      ┣ style.css
      ┗ vite-env.d.ts
    

테스트

실제 테스트를 해보기위해 테스트 환경을 구축해본다.

전역 패키지 등록

패키지 테스트를 하기 위해 npm link 를 이용해서 다음과 같은 테스트 환경을 구축해보기로 하자.

  1. @ui-component/elements 를 전역 패키지로 등록
### @ui-component/elements 프로젝트 루트에서 실행.
npm link
npm list -g
# ~/.nvm/versions/node/v18.19.0/lib
# ├── @ui-component/elements@0.0.0 -> ~/ui-component/packages/elements
# ├── corepack@0.22.0
# └── npm@10.2.3

npm list -g 명령어로 전역 패키지로 등록된 것을 확인할 수 있다.

테스트용 서비스 앱 구축

공통 컴포넌트를 실제 서비스에 호출해서 잘 동작되는지 확인하기 위해

간단하게 nuxt 프로젝트를 구축하여 테스트해보았습니다.

테스트용 서비스 앱 기술스택

  • UI F/W : nuxt3
  • Build Tool : vite

테스트용 서비스 앱 구축

  1. nuxt starter를 이용하여 신규 프로젝트 생성

     pnpm dlx nuxi@latest init sample-app
     # Packages: +2
     # ++
     # Progress: resolved 2, reused 2, downloaded 0, added 2, done
     # 
     # ✔ Which package manager would you like to use?
     # pnpm
     # ◐ Installing dependencies...
     # Scope: all 3 workspace projects
     # . postinstall$ nuxt prepare
     # │ ✔ Types generated in .nuxt
     # └─ Done in 5.1s
     # 
     # dependencies:
     # + nuxt 3.11.2
     # + vue 3.4.27
     # + vue-router 4.3.2
     # 
     # Done in 50.5s
     # ✔ Installation completed.
     # 
     # ✨ Nuxt project has been created with the v3 template. Next steps:
     #  › cd client
     #  › Start development server with pnpm run dev   
    
  2. @ui-component/elements 모듈 추가 (전역에 등록된 모듈을 연결해준다)

     npm link @ui-component/elements
     # added 29 packages, and audited 31 packages in 8s
     # 
     # 4 packages are looking for funding
     #   run `npm fund` for details
     # 
     # found 0 vulnerabilities
     npm list
     # nuxt-app@ ~/sample-app
     # ├── @ui-component/elements@0.0.0 extraneous -> ~/ui-component/packages/elements
     # ├── nuxt@3.11.2
     # ├── vue-router@4.3.2
     # └── vue@3.4.27
    

    npm list 명령어로 전역 패키지로 등록된 것을 확인할 수 있다.

  3. 샘플 코드 작성

     // App.vue
    	
     <template>
       <Page></Page>
     </template>
    	
     <script setup>
     import '@ui-component/elements/dist/style.css';
     import {Page, Button, Header} from '@ui-component/elements';
     </script>
    
  4. 확인

    1Untitled.png

💡
@ui-component/elements 의 배포 내용이 변경될 경우, 서버를 다시 실행해주어야 적용됨.

Error Reporting

  1. Cannot read properties of null (reading ‘matches’)

    에러 내용:

     npm ERR! Cannot read properties of null (reading 'matches')
    

    해결 방안:

    node_modules 폴더를 지우고 다시 설치해준다.

  2. The symbol “h” has already been declared

    에러 내용:

     [plugin:vite:client-inject] Transform failed with 1 error:
     ~/ui-component/packages/elements/dist/elements.es.js:15:135: ERROR: The symbol "h" has already been declared
    

    해결 방안:

    nuxt3의 경우 자동으로 모듈을 import하는 auto-imports 기능과 충돌 발생,

    nuxt.config.ts 파일에 아래 auto-imports 옵션에 예외를 추가해준다.

     // nuxt.config.ts
    	
     //...
     imports: {
       transform: {
         exclude: [/\ui-component\b/]
       }
     },
     //...
    

Refs.