본문 바로가기메뉴 바로가기


FE개발자의 성장 스토리 03 : 카카오 어드민 UI 컴포넌트를 모노레포로 개발하여 얻은 것들

안녕하세요. FE플랫폼팀 odd입니다.

FE플랫폼팀에서는 카카오 전사의 FE 개발 환경을 개선하기 위해 어드민 UI 컴포넌트를 개발하고 있습니다. 이번 글에서는 개발 과정에서 모노레포 환경을 통해 개발한 내용을 공유드리고자 합니다.


어드민 UI 컴포넌트

어드민 UI 컴포넌트는 카카오 서비스들의 운영에 필요한 어드민 페이지 개발을 쉽게 할 수 있게 해주는 UI 컴포넌트 프로젝트입니다. 카카오의 사내 디자인 시스템을 적용해 어드민 개발 시 디자인에 대한 고민을 없앴고, 프론트엔드 개발자 뿐만 아니라 프론트엔드에 익숙하지 않아도 개발할 수 있도록 리액트, 바닐라 자바스크립트 컴포넌트를 제공하고 있습니다. 추후 뷰 컴포넌트를 제공할 계획도 가지고 있습니다.

고민거리 – 여러 UI 환경의 컴포넌트를 제공하기

프로젝트 초기에 여러 UI 환경의 컴포넌트를 제공하기 위해 고민했던 부분 중 하나는 데이트피커와 같은 비교적 복잡한 컴포넌트를 설계하는 부분이었습니다.

예를 든 데이트피커는 날짜를 선택하는 기본 기능 이외에도 여러 부가 옵션이 많이 있습니다(일반 선택, 기간 선택, 선택 불가 날짜 지정, 선택 가능 최소 최대 날짜 지정 등). 이와 관련된 세부 로직을 각각의 UI 환경마다 새롭게 구현한다는 것은 관리 측면에서 올바르지 않다는 생각을 했습니다.

이에 대한 일반적인 대안은 바닐라 환경으로 데이트피커를 만들고, 이를 나머지 UI 환경에서 래핑 해서 사용해 중복 구현을 하지 않는 것 일 겁니다. 이 대안은 초기에 고려했지만, 데이트피커 외에도 여러 많은 복잡한 컴포넌트가 존재했고 각각의 환경에 맞게 렌더링 하는 것이 더 적합한 컴포넌트가 존재해 다른 방안을 생각해봤습니다.

코어로직과 렌더로직 분리하기

이에 실제 화면을 그리는 일은 제외하며 컴포넌트의 로직을 가진 객체를 만들고, 각각의 환경에서 이 객체를 활용해 화면을 그리고 객체를 제어하며 인터렉션 한다면 조금은 효율적일거란 생각을 가졌습니다.

이에 이런 형태의 화면을 그리는 일을 직접 하지 않는 컴포넌트 객체들은 core라는 이름의 패키지로 관리하고, 각각의 환경에서 react, html 등에서 이 패키지를 의존해 화면을 그리는 컴포넌트를 만들게 됐습니다.

이에 다음과 같이 여러 개의 패키지를 만들었습니다. 현재 카카오에서는 사내 npm 레지스트리가 존재하며 @kakao 스코프로 정의하고 있습니다.

  • @kakao/admin-ui-core (컴포넌트들의 UI를 제외한(분리한) 로직을 가지고 있는 패키지)
  • @kakao/admin-ui-react (React 환경에서 core 컴포넌트를 활용해 UI를 담당하는 패키지)
  • @kakao/admin-ui-vue (예정) (Vue 환경에서 core 컴포넌트를 활용해 UI를 담당하는 패키지)
  • @kakao/admin-ui-html (바닐라 JS 환경에서 core 컴포넌트를 활용해 UI를 담당하는 패키지)

모노레포 (Monorepo)

Monorepo는 여러 프로젝트를 하나의 저장소로 개발하는 방식입니다.

어드민 UI 컴포넌트는 위에서 소개한 여러 패키지를 개발하고 관리하는 방법으로 모노레포 환경을 선택했고, 사내 깃헙 저장소에 다음과 같이 관리되고 있습니다.

admin-ui/
  package.json (저장소 루트의 전체 프로젝트 관리용 package.json, 일반적으로 "private": true로 정의)
  packages/
    core/
      package.json
    html/
      package.json
    react/
      package.json

어드민 UI 컴포넌트 프로젝트가 모노레포를 통해 얻은 것 1 – 개발 과정에서의 용이한 의존성 관리

앞에서 소개 드린 대로 어드민 UI 컴포넌트는 컴포넌트의 UI를 제외한 core 패키지와 이 core 컴포넌트를 활용해 UI를 담당하는 패키지가 있습니다. UI를 담당하는 패키지들은 core 패키지를 의존하기에 package.json에는 다음과 같은 의존성이 정의되어 있습니다.

{
  "name": "@kakao/admin-ui-react",
  "version": "0.3.1",
  "dependencies": [
    "@kakao/admin-ui-core": "0.3.1"
  ]
}

로컬에서 개발할 때, core의 변경 내용을 UI 담당 패키지에서 사용하기 위해서는 UI 담당 패키지의 node_modules 디렉터리에 있는 core를 로컬에서 개발 중인 core의 심볼릭링크로 설정해야 합니다. 심볼릭링크는 수동으로 직접 생성하거나 npm link 와 같은 커맨드를 통해 생성할 수 있습니다. 이렇게 node_modules 디렉터리에 생성된 심볼릭링크는 npm install 커맨드가 실행되면 npm 레지스트리로부터 다운로드한 패키지로 덮어지기에 이 부분에 대한 관심이 필요합니다.

@kakao/admin-ui-react/
  node_modules/
    @kakao/admin-ui-core (로컬 어딘가에 있는 @kakao/admin-ui-core의 심볼릭링크)

모노레포로 구성된 어드민 UI 컴포넌트는 이런 작업을 저장소 내에 다음과 같은 작업을 하는 스크립트를 통해 자동화할 수 있었고 이에 대한 고민과 주의를 줄일 수 있었습니다.

1. 모노레포 내 모든 패키지들에서 npm install 커맨드 실행
2. 모노레포 내 연관된 패키지들의 심볼릭링크 설정

이런 스크립트 작업이 가능했던 이유는 패키지들과 스크립트가 하나의 저장소에서 함께 관리되기 때문으로, 만약 패키지들이 각각의 저장소로 관리되고 있었다면 각각의 저장소가 로컬에서 일관된 위치에 있다는 것을 보장할 수 없어 안정적인 스크립트 생성이 불가했을 겁니다.

어드민 UI 컴포넌트 프로젝트가 모노레포를 통해 얻은 것 2 – 프로젝트의 여러 설정을 일관되게 적용

어드민 UI 컴포넌트는 여러 도구들을 이용해 만들어지고 있습니다.

  • ESLint, Prettier를 이용한 코드 품질 향상
  • Husky, Lint-Staged를 통한 커밋 훅의 린트, 포멧터 적용
  • TypeScript, Babel을 이용한 트랜스 파일링
  • Webpack을 이용한 번들링
  • Jest를 통한 테스트
  • Storybook을 이용한 UI 개발 환경
  • 그 외 등등

이런 도구들을 설정하고 사용하는 것이 하나의 저장소에서 이루어지기에, 도구들의 기본 설정과 이를 각 패키지마다 확장시켜 사용하도록 해 설정의 중복을 줄이고 재사용성을 높일 수 있었습니다. 또한 개발 단계에서 쓰이는 이런 도구들을 저장소 루트 package.json의 devDependencies에 놓고 사용함으로써 패키지들이 동일한 버전의 도구들을 사용하도록 해 외부 의존성 관리도 좀 더 수월하게 할 수 있었습니다.

어드민 UI 컴포넌트 프로젝트가 모노레포를 통해 얻은 것 3 – 패키지 간의 연관된 모든 작업들이 하나의 커밋으로 이루어진다.

어드민 UI 컴포넌트는 각각의 패키지를 따로 개발하는 대신 동시다발적인 개발이 필요했습니다. 의존 관계로 인해 다른 패키지에 영향을 주는 일이 빈번했고, package.json에 version을 변경할 때도 패키지마다 적절한 작업이 필요했습니다.

모노레포로 구성된 어드민 UI 컴포넌트에서는 패키지들의 연관된 작업들이 하나의 커밋으로 이루어지고 있습니다. 브랜치 또한 하나의 브랜치를 통해 여러 패키지들의 작업을 진행할 수 있었습니다. 이로 인해 패키지들에 분포된 연관된 작업들의 변경사항을 추적하고 관리하기가 수월했습니다. 만약 패키지들이 각각의 저장소를 가지고 있었다면 패키지들 간에 연관된 변경사항을 추적하기에 좀 더 많은 제어와 관심이 들었을 것입니다.

어드민 UI 컴포넌트 프로젝트가 모노레포를 통해 얻은 것 4 – 테스트, 배포 등의 용이성

현재 어드민 UI 컴포넌트는 패키지마다 컴포넌트의 검증을 위한 유닛 테스트, 스냅샷 테스트, 시각적 회귀 테스트, E2E 테스트가 존재합니다. 모노레포 환경을 통해 모든 패키지의 테스트를 실행하는 스크립트를 생성하고 사용함으로써 전체 프로젝트의 검증을 비교적 쉽게 할 수 있으며 이에 CI 환경도 비교적 쉽게 구축할 수 있었습니다.

사내 NPM 레지스트리로의 배포 또한 모든 패키지들의 배포를 함께 진행하는 스크립트를 만들어 진행할 수 있었고, 이외에도 여러 스크립트를 만들어 프로젝트의 관리와 CI/CD 환경 구축을 비교적 쉽게 할 수 있었습니다.

Lerna – 어드민 UI 컴포넌트 프로젝트에서 사용한 모노레포 관리 도구

어드민 UI 컴포넌트의 모노레포 환경은 Lerna라는 모노레포 관리 도구를 이용해 구성되어 있습니다. Lerna는 git과 npm 기반 모노레포 환경의 작업을 도와주는 도구로 다음의 기본 동작을 포함한 여러 커맨드를 가지고 있습니다. 어드민 UI 컴포넌트에서는 Lerna의 여러 기능을 활용하고 조합해 개발을 진행하고 있습니다.

의존성 연결
- lerna bootstrap (각 패키지의 npm install 실행 후 모노레포 내 연관된 패키지의 심볼릭링크 설정)
실행
- lerna run (각 패키지의 package.json에 정의된 scripts를 실행)
배포
- lerna publish (배포를 하기 위해 패키지들의 버전을 변경하고 npm에 배포)

어드민 UI 프로젝트에서는 Lerna에서 제공하는 기능을 조합하여 프로젝트 개발을 진행하고 있습니다.

이외에도, 어드민 UI 컴포넌트는 npm을 사용하고 있으나 yarn에는 패키지 매니저로써 모노레포 내 멀티 패키지에 대한 의존성 관리를 도와주는 workspaces라는 기능이 있습니다(npm에도 7부터 유사한 workspaces 기능이 생겼습니다). yarn과 lerna는 비 배타적인 관계로 함께 사용할 수도 있습니다.

마치며

카카오 어드민 UI 컴포넌트를 모노레포로 개발하며 경험한 것은 결국 하나의 저장소입니다. 의존 관계에 있는 프로젝트들을 하나의 저장소를 통해 파악 및 관리를 할 수 있고, 하나의 저장소만 신경 쓰면 되기에 프로젝트가 늘어나도 저장소로부터 파생되는 여러 도구들(git 클라이언트, 리모트 저장소 플랫폼, CI, CD 등)에 대한 신경을 줄이며 좀 더 개발에 집중할 수 있는 환경이 될 수 있다고 생각됩니다.

이 글을 통해 어드민 UI 컴포넌트와 같은 비슷한 환경의 프로젝트를 진행하실 때 조금이나마 도움이 되었으면 좋겠습니다.

감사합니다.


✔️ 함께 하면 좋은 글

odd.laranhee
odd.laranhee 자바스크립트와 웹으로 카카오를 만들고 있습니다.
Top Tag
2021
2021-new-krew
adaptive-hash-index
adt
agile
agilecoach
ai
Algorithm/ML
Algorithm/Ranking
almighty-data-transmitter
Analyzer
android
angular
anycast
App2App
applicative
Architecture
arena
ast
async
aurora
babel
babel7
Backend
BApp
bgp
big-data
ble
blind-recruitment
block
Block Chain
blockchain
bluetooth
brian
business
Cache
cahtbot
Caver
cd
CDR
ceph
certificate
certification
cgroup
chrome
ci
cite
client
clojure
close-wait
cloud
cloudera-manager
clustered-block
cmux
cnn
code-festival
code-review
codereview
coding
coding test
competition
Compliance
component
conference
consul
container
contents
contest
cookie
core-js@3
Corporate Digital Responsibility
couchbase
COVID-19
cpp
Data
data-engineering
DB
deep-learning
Dependency
dependency-graph
dev
dev-session
dev-track
developer
developer relations
developers
devops
digitalization
digitaltransformation
dns
docker
dr
employeecard
eslint
Feature List
Featured
friendstime
front-end
frontend
functional-programming
funfunday
fzf
garbage-collection
gawibawibo
GC
github
globalpollution
go
graphdb
graphql
Ground X
growth
ha
hadoop
hate speech
hbase
hbase-manager
hbase-region-inspector
hbase-snashot
hbase-table-stat
hbase-tools
hri
id
if kakao
ifkakao
infrastructure
innodb
internship
ios
item
Java
javascript
jsconf
jsconfkorea
json
k8s
kafka
kakao
kakao-Career-Boost-Program
kakao-commerce
kakao-games
kakaoarena
kakaocommerce
kakaocon
kakaoenterprise
kakaok
kakaokey
kakaokrew
kakaomap
kakaotalk
KAS
KCDC
khaiii
Klaytn
Klip
kubernetes
l3dsr
l4
License
links
Linux
load-balancing
machine-learning
marathon
meetup
melon
mesos
message
Messaging
microservice
mobil
monad
monorepo
mtre
mysql
mysql-realtime-traffic-emulator
nand-flash
network
new
new-krew
nfc
nomad
ocp
olive
onboarding
open
open source
opensource
openstack
OpenWork
OSS
page
parallel
PBA
planning poker
Platform
polyfill
programming-contest
project-structure
pycon
python
quagga
react
reactive-programming
reactor
recap
recommendation
recommendation system
recruitment
redis
redis-keys
redis-scan
related-blind
rest
rubics
ruby
rxjs
s2graph
scala
scalaz
seminar
Serve
server
service
sharding
shopping
socket
spark
spark-streaming
SpringBoot
ssd
Statistics/Analysis
Stomp
storage
storm
style-guide
summer internship
support
System
talk
talkchannel
tcp
tech
Techtalk
test
Thread-Debugging
time-wait
tmux
typescript
Untact
update
User Story
vim
vim-github-dashboard
vim-plugin
vue
vue.js
web-cache
webapp
WebSocket
weekly
work
workplatform
라이선스
오픈소스
오픈소스검증
의존성분석
All Tag
2021
2021-new-krew
adaptive-hash-index
adt
agile
agilecoach
ai
Algorithm/ML
Algorithm/Ranking
almighty-data-transmitter
Analyzer
android
angular
anycast
App2App
applicative
Architecture
arena
ast
async
aurora
babel
babel7
Backend
BApp
bgp
big-data
ble
blind-recruitment
block
Block Chain
blockchain
bluetooth
brian
business
Cache
cahtbot
Caver
cd
CDR
ceph
certificate
certification
cgroup
chrome
ci
cite
client
clojure
close-wait
cloud
cloudera-manager
clustered-block
cmux
cnn
code-festival
code-review
codereview
coding
coding test
competition
Compliance
component
conference
consul
container
contents
contest
cookie
core-js@3
Corporate Digital Responsibility
couchbase
COVID-19
cpp
Data
data-engineering
DB
deep-learning
Dependency
dependency-graph
dev
dev-session
dev-track
developer
developer relations
developers
devops
digitalization
digitaltransformation
dns
docker
dr
employeecard
eslint
Feature List
Featured
friendstime
front-end
frontend
functional-programming
funfunday
fzf
garbage-collection
gawibawibo
GC
github
globalpollution
go
graphdb
graphql
Ground X
growth
ha
hadoop
hate speech
hbase
hbase-manager
hbase-region-inspector
hbase-snashot
hbase-table-stat
hbase-tools
hri
id
if kakao
ifkakao
infrastructure
innodb
internship
ios
item
Java
javascript
jsconf
jsconfkorea
json
k8s
kafka
kakao
kakao-Career-Boost-Program
kakao-commerce
kakao-games
kakaoarena
kakaocommerce
kakaocon
kakaoenterprise
kakaok
kakaokey
kakaokrew
kakaomap
kakaotalk
KAS
KCDC
khaiii
Klaytn
Klip
kubernetes
l3dsr
l4
License
links
Linux
load-balancing
machine-learning
marathon
meetup
melon
mesos
message
Messaging
microservice
mobil
monad
monorepo
mtre
mysql
mysql-realtime-traffic-emulator
nand-flash
network
new
new-krew
nfc
nomad
ocp
olive
onboarding
open
open source
opensource
openstack
OpenWork
OSS
page
parallel
PBA
planning poker
Platform
polyfill
programming-contest
project-structure
pycon
python
quagga
react
reactive-programming
reactor
recap
recommendation
recommendation system
recruitment
redis
redis-keys
redis-scan
related-blind
rest
rubics
ruby
rxjs
s2graph
scala
scalaz
seminar
Serve
server
service
sharding
shopping
socket
spark
spark-streaming
SpringBoot
ssd
Statistics/Analysis
Stomp
storage
storm
style-guide
summer internship
support
System
talk
talkchannel
tcp
tech
Techtalk
test
Thread-Debugging
time-wait
tmux
typescript
Untact
update
User Story
vim
vim-github-dashboard
vim-plugin
vue
vue.js
web-cache
webapp
WebSocket
weekly
work
workplatform
라이선스
오픈소스
오픈소스검증
의존성분석

위로