274개의 댓글을 거쳐 1시간 반만에 배포한 모노레포 이야기
서비스와 백오피스의 공통 모듈을 분리하여 모노레포를 구축했다.
CS Broker는 문제풀이 서비스를 운영하기 위한 문제 관리, AI 데이터 관리를 위한 백오피스도 직접 개발하여 사용하고 있다. 원래는 airtable로 문제와 유저답변을 관리하고, label studio로 AI 학습 데이터에 대한 라벨링을 진행했다. 하지만 앞선 방식으로는 데이터도 분산되고, 관리도 어려우며 앞으로 다른 기능들이 추가되었을때마다 다른 도구를 도입해야한다는 문제점 때문에 직접 백오피스를 구현하기로 했다. 다만 특정 기간까지 마무리해야하는 서비스단 기능들이 있어서, 2주짜리 스프린트 내로 백오피스 개발을 빠르게 마무리짓기로 했다. 이때 서비스는 직접 컴포넌트를 개발했고, 백오피스는 MUI를 사용할 예정이었으며 API도 분리되어 개발될 예정이었으므로 새로운 레포지토리를 생성해 개발을 시작했다.
그러나 추후 일부 API 스펙이 변경되면서 문제가 발생했다. 인증 부분과 API 코드가 서비스와 관리자 페이지에서 일부 중복되어 관련 type과 코드를 양쪽 레포에서 모두 변경해야 했기 때문이다. (로그인, 문제 세트 검색 API & types, 사용자 인증 util 함수 등)
같은 내용을 여러 번 수정하는 것보다 중복 코드를 공통으로 사용할 수 있도록 모노레포로 변경하는 것이 좋겠다는 판단을 내렸다.
모노레포는 관리 포인트를 단일화하고 의존성 관리를 용이하게 한다는 점에서 자주 사용되는 소프트웨어 개발 전략이다. 사랑받는 만큼 모노레포 구축 도구의 선택지도 다양하다. 관련 라이브러리 중에서는 아래와 같이 lerna, turborepo, nx, rush가 자주 언급되는 선택지이다.
이들을 퍼포먼스, 사용성 등으로 비교하는 글은 구글링을 통해 여러개 찾아볼 수 있다. 다만 필자는 여러 모노레포 구축 방법들을 볼때 각각의 기술적 강점, 프로젝트 적합성, 마이그레이션 공수를 기준으로 비교해보았다. 예를 들어 Lerna의 경우 versioning, publishing 기능에 강점을 가지나, 이를 활용하지 않을 것이라 제외했고, turborepo의 경우 캐싱과 병렬처리를 통한 빌드 타임 감소가 특징적이지만, 현재 프로젝트의 빌드 타임은 서비스와 관리자 페이지가 각각 약 41초, 32초 정도로 문제시될 정도로 길지 않아 오버 엔지니어링이라 판단했다. 비교 및 분석 결과 당장은 모노레포 구축을 위해 빌드 도구를 도입할 필요가 없다고 생각하여 모듈화에 초점을 두고 패키지 매니저의 workspace를 사용하는 것으로 결정했다.
그렇다면 이제 중복된 코드를 분리하고, 공통 패키지를 만들어 서비스와 백오피스가 사용할 수 있도록 만들어주면 그만이다. 하지만 정확히 어디까지가 중복 코드일까? 처음에는 “인증쪽 코드만 분리하면 되겠다” 라고 생각하고 시작된 작업은 생각보다 녹록치않았다. 서비스와 백오피스는 아래와 같이 똑같은 형태의 axios 인스턴스를 사용하고 있었다. 따라서 이것을 공통 코드로 분리했다.
const apiClient = axios.create({
baseURL: `${API_BASE_URL}/api`,
withCredentials: true,
});
apiClient.interceptors.request.use((config) => {
const userInfo = getUserInfo();
if (userInfo) {
config.headers[AUTHORIZTION] = BEARER_TOKEN(userInfo.accessToken);
}
return config;
});
apiClient.interceptors.response.use(
// 응답 데이터 처리
);
API들을 랲핑한 함수도 다음과 같이 똑같은 형태였다. 이것들도 공통 함수일 수 있다.
export const authApiWrapper = {
login: (data: ILoginRequest) => {
return apiClient.post(API_URL.LOGIN, data).then(res) => res.data);
}
};
API들을 공통으로 사용할거라면 type들도 공통으로 사용하게 된다. 또 해당 API를 사용하기 위해 필요한 util 함수들도 공통이다. constants들도 마찬가지이다. 에러처리도 같은 방식으로 해야한다. 또…………..
라고 생각하다보니 끝도 없어졌다. 다만 의문점이 생겼다.
과연 공통코드가 많으면 많을수록 좋은 설계일까?
위의 고민을 함께 해준 팀원이 common 모듈의 저주에 대해 이야기해주었다. 공통 모듈을 생성함과 동시에 점점 공통 모듈이 짊어지게 되는 짐은 커진다. 이는 추후 기능을 수정하고 싶을 때, 리팩토링이 필요할 때 시스템 전체를 건드리도록 설계되어 필연적으로 문제 발생 확률을 높인다. 따라서 공통모듈은 딱 하나의 도메인만 담당하도록 설계하기로 했다. 오직 인증 관련 util, type, constants 들만 분리하도록 했다.
생각해보면 어떨 때는 “아 너무 크네” 하고 별생각없이 분리하고, 또 어떨 때는 “이거 비슷한거 있는데” 하면서 분리하기도 하고 이유가 다양했다. 코드를 분리할 때는 명확한 기준이 있으면 좋을 것 같다. 가독성을 위한 것일 수도 있고, 재사용을 위한 것일 수도 있고, 또 역할 분리를 위한 것일수도 있다. 중요한건 이 기준을 자기 자신이 인지하고 수행하는 것이다. (컴포넌트면에서 이와 관련된 유익한 내용이 포함된 발표가 있다.)
구조를 변경한만큼 배포도 신경쓸 점들이 많았다. 먼저 lint를 최상위에서 관리하도록 변경했다. 특히나 다음과 같이 git-diff에 대한 lint를 확인하는 스크립트의 경우 git repository 하나에 전체가 포함된 만큼 최상위에서 관리하는 것이 적절했다.
eslint $(git diff --name-only --diff-filter=duxb origin/$BASE origin/$HEAD | grep -E '\\.((j|t)sx?)$' | xargs)
다른 공통 라이브러리들은 netlify에서 언급한대로 최상위로 올리지 않았지만, devdependencies에 설치하는 경우는 배포될때 포함되지 않으므로 관계가 없어 eslint도 같은 맥락으로 최상위에 설치해도 괜찮았다.
이후 액션 파일에서 github secret과 dist 경로를 변경했다. 이때 궁금했던건 dist 경로의 경우 자동으로 배포 전에 붙는 Netlify Preview와 실제로 배포된 사이트가 각각 yml 파일에 설정한 경로와 Netlify UI에서 설정한 경로 중 어느 쪽을 참고하는지 였다. 결론적으로는 preivew는 당연하게도 netlify UI를 따랐다. 아직 action이 실행되기 전에 보여주는 것이니 당연한 결과 배포된 사이트는 아래의 deploy log에 따르면 원래는 Netlify UI에서 설정한 경로를 보려고 했는데 config가 있다며 이를 따르겠다고 한다. 액션 파일에 설정해준 경로를 우선시하는 모습이다.
이렇게 여러 과정을 거쳐 모노레포를 구축하고 배포했다! 해보고 나니 몇가지 장단점을 직접 느낄 수 있었다.
소프트웨어 설계에 정답은 없지만 최적의 솔루션을 찾아가는 과정은 꼭 필요하다.
https://techblog.woowahan.com/2637/
https://www.youtube.com/watch?v=fR8tsJ2r7Eg
https://docs.netlify.com/configure-builds/monorepos/
"Use npm workspace to set up a monorepo for managing multiple related packages. This allows for easier development, testing, and deployment, and reduces duplication of code and dependencies. Consider using a tool like Lerna to simplify management." MyHealthAtVanderbilt Login
최고 최고 멋져요!!!!!!!!!!!!!! 너무 고생하셨어요!!!