3. OMCT 폴더 구조 - SaekKkanDa/OppaManyColorTone GitHub Wiki
혼자 개발한다면 폴더 구조는 그렇게 중요하지 않을 수 있습니다. 어떤 코드가 어디에 위치 하는지 파악을 하고 있기 때문입니다.
하지만, 많은 개발자가 협업을 하게 된다면 다릅니다. 다른 팀원들이 작성한 코드를 전부 파악할 수 없기 때문에 필요한 로직이 이미 구현되어 있는지 찾는 것은 많은 비용을 사용하게 됩니다. 결국 찾지 못하여서 중복되는 로직을 가진 모듈이 만들어지는 일은 많이 경험 해보았을 것 입니다.
그래서, 내가 개발한 코드가 다른 팀원들에게 잘 사용되기를 원한다면 어떻게 해야 할까요? 그 팀원에게 코드에 대한 설명이 필요할 것 입니다. 그리고 이러한 설명은 비용입니다.
비용을 줄이기 위해서 어떤 폴더에는 어떠한 로직이 있다는 것을 규칙으로 정하게 된다면 비용을 크게 절감 할 수 있을 것 입니다.
다시 말하면, 협업 과정에서 폴더 구조는 중요합니다. 이 디렉토리에는 이러한 코드가 포함되어 있다는 약속을 하게 된다면 위에서 말한 비용을 크게 절감 할 수 있습니다.
제가 생각하는 협업의 중요한 점 중 하나는 팀원들이 뇌를 비우고 코딩을 할 수 있게 하는 것 입니다. (가장 중요한 점은 병렬로 일할 수 있게 하는 것)
먼저, 프론트엔드에서 사용되는 모듈(컴포넌트, 훅, 함수 등)을 3가지 기준에 맞게 분리를 하였습니다. 분리된 모듈들이 재사용성은 어떤지, 코드 퀄리티는 어떤지, 얼마나 많은 비용을 투자 해야하는지(우선순위)를 고려하여 봐주시기 바립니다.
프론트엔드에서 사용되는 컴포넌트 또는 함수를 3가지 기준으로 분리 해보았습니다.
- Domain에 의존하는지
- Framework에 의존하는지
- UI에 관련된 것 인지 Logic에 관련된 것인지
키워드만 봐도 대충 이해하실 수 있지만 각각에 대한 부연설명을 하자면
도메인에 의존하는지 여부 입니다. 도메인에 의존한다면 해당 애플리케이션에서만 사용될 수 있어 재사용성이 크게 떨어지게 됩니다.
예를 들어서 OMCT 프로젝트 색을 고르는 로직이 도메인에 종속 되어있다고 볼 수 있습니다. 때문에 웬만하면 다른 프로젝트에서 사용하기는 어렵습니다.
react, vue 등의 프레임워크에 의존하는지 여부 입니다. react에 의존한다면 vue를 사용하는 애플리케이션에서는 사용하지 못합니다. domain 종속 여부에 비해서 재사용성은 높습니다.
react를 사용하는 애플리케이션에서는 버튼과 같은 다양한 기본 컴포넌트와 커스텀 훅 등이 속하게 됩니다.
tanstack-query, react-form 같은 라이브러리에서는 vue와 react와 같은 프레임워크에서 자유롭게 구현하기 위해서 순수 자바스크립트 로직으로 core를 구성 후 각각의 프레임워크에 맞게끔 래핑하여 배포하게 구현되어있습니다.
말 그대로 UI에 관련된 컴포넌트인지 Logic에 관련된 컴포넌트인지 분류 할 수 있습니다.
UI에 관련된 로직은 Button 컴포넌트, Logic은 커스텀 훅 또는 유틸 함수가 포함 됩니다.
위의 내용은 아래 표로 요약할 수 있습니다.
Domain Dependency | Framework Dependency | UI Logic | Example | Reusable | Note | Label |
---|---|---|---|---|---|---|
O | O | UI | - react component (CelebImage, ShareButton...) | L | storybook | (A) |
O | O | Logic | - react component (Context, Boundary...) - react hook (data fetching..., useChoice...) | L | (B) | |
O | X | UI | X | |||
O | X | Logic | - JavaScript functions .logic.tsx | L | colocation declaritive | (C) |
X | O | UI | - headless component (useSelect, useInput ...) | M | headless component | (D) |
X | O | Logic | - react hook (useScrollTop, useOutsideClick...) - utility component (Modal, Toast, Toolip...) | M | (E) | |
X | X | UI | X | |||
X | X | Logic | - utility functions | H | unit test | (F) |
각각의 특징에 대해서 알아보겠습니다.
여기에 속한 컴포넌트는 domain과 framework에 의존해서 react를 사용하는 OMCT 애플리케이션에서만 사용이 되기 때문에 재사용성이 낮습니다.
그러므로 이러한 컴포넌트에 많은 로직과 정보를 포함하는 것보다는 재사용성이 높은 다른 컴포넌트(D)를 래핑해서 사용하는 것이 좋습니다. (재사용성이 높은 컴포넌트는 아래에 설명)
하지만 여기에 속한 컴포넌트는 디자이너가 제공하는 디자인 시안과 밀접하기 때문에 디자인 QA를 위한 storybook을 제공할 예정이라면 해당 컴포넌트는 스토리를 가지고 있는 것을 추천 드립니다.
즉, A 컴포넌트에 많은 공수를 들이고 싶지 않다면 기반이 되는 컴포넌트(D)를 잘 만들어야 합니다.
A와 마찬가지로 domain과 framework에 의존하여 재사용성이 낮은 컴포넌트가 속하게 됩니다. (A)와 다른점은 UI가 아닌 Logic에 관련된 컴포넌트입니다. 때문에 storybook을 이용할 수 없습니다.
여기에 속하는 컴포넌트는 OMCT 애플리케이션에서 사용되는 데이터를 가져오거나 useChoice(가상의 훅) 같은 커스텀 훅이 속하게 됩니다.
framework에 의존하지 않지만 domain에 의존하기 때문에 OMCT 애플리케이션 외에는 사용되기가 어렵습니다.
여기에 속하는 함수는 (A) (B) 구현에 사용되는 로직들이 포함 됩니다. (A)(B) 컴포넌트를 declarative(선언형) 하게 구현하기 위해서 관련된 로직을 해당 컴포넌트와 같은 hierarchy 구조에 *.logic.tsx 파일의 형태로 분리 합니다.
선언형 프로그래밍 제가 생각하는 선언형 프로그래밍은 추상화 입니다. 추상화를 통한 팀원이 코드를 깊게 분석하는데 들이는 비용을 줄일 수 있게 해야 합니다. (우리가 브라우저, react가 어떻게 돌고 있는지 정확히 몰라도 코딩을 할 수 있는 것 처럼)
Colocation 여기에 속한 함수들은 재사용성이 낮기 때문에 팀원에게 노출되지 않아야하며 (A) (B)는 여기에 속한 함수에 강하게 의존하기 있기 때문에 colocation 하게 같은 폴더에 위치 시킵니다.
framework에 의존하지만 domain에 의존하지 않는 컴포넌트이며 많은 react UI 프레임워크가 여기에 속하게 됩니다. (MUI, Bootstrap …)
컴포넌트를 개발하는 다양한 방법 중 UI에 100% 유연성을 가진 headless 컴포넌트를 사용하는 것을 추천 합니다. 비즈니스 로직과 뷰로직을 완벽히 분리하고 컴포넌트를 controll하게 만드는 것이 변화(요구사항의 변경)에 유연하기 때문입니다. (예를 들어서 <List/>
컴포넌트 내부에 드롭다운을 열고 닫는 isExpand
상태 값을 가지고 있었는데 isExpand
여부를 localStorage에 저장하여 사용하게 바뀐다면?)
아래 headless compoent의 예시를 들겠습니다.
아래는 useSelect
커스텀훅을 생성하여 리턴값을 어떠한 Select 컴포넌트에 전달하였습니다.
function Page1() {
const select1 = useSelect({defaultValue: 1, validationFn: () => ERROR.NO_ERROR});
const select2 = useSelect({defaultValue: 2, validationFn: () => ERROR.NO_ERROR});
return (
<>
<CustomSelect1 {...select1} />
<CustomSelect2 {...select2} />
</>);
}
useSelect
훅은 위와 같이 UI에 100% 유연하게 되며 <CustomSelect1/>
<CustomSelect2/>
와 같은 컴포넌트의 props만 useSelect
와 맞추게 된다면 <Page1/>
컴포넌트는 select를 control 할 수 있게 됩니다.
(D)와 마찬가지로 많은 react 라이브러리에서 볼 수 있습니다.(redux, react-query, react-form 등) react에 의존하는 애플리케이션에 사용되는 커스텀 훅 또는 <Modal/>
<Toast/>
와 같은 범용적인 컴포넌트가 여기에 속하게 됩니다.
useDetectOutsideClick
useScroll
와 같은 느낌적으로 react를 사용하는 어디든에서 사용될 것 같은 커스텀 훅과 컴포넌트가 속하게 됩니다.
domain, framework에 자유로운 순수 JavaScript 함수가 여기에 속하게 됩니다. 이 또한 많은 라이브러를 볼 수 있습니다. (axios … 등).
어디에서든 사용될 수 있기 때문에 가장 재사용성이 높습니다. 그러므로 가장 신뢰성이 있어야 합니다. (util 함수에서 문제가 발생 시 대부분의 애플리케이션 로직에서 문제가 발생하기 때문 입니다.)
가장 신뢰성 있는 코드를 작성해야 하기 때문에 만약 테스트코드를 작성해야 한다면 여기에 속한 모듈을 가장 우선순위를 높게 해야 합니다.
(A) ~ (F)를 종합하여 아래와 같은 폴더 구조를 만들었습니다.
**base**
- utils --> (F)-Utility Javascript Functions - with test code
- hooks --> (D)-Headelss Component
- components --> (E)-Utility React Components
**omct**
- hooks --> (B)-React Logic Components
- components --> (A)-React UI Components
- {component}.logic.tsx --> (C)-Component Logic functions
- pages
- {component}.logic.tsx --> (C)-Component Logic functions
- constants
- stores
- ...
도메인 종속 여부로 base와 omct 디렉토리로 나누었습니다.
base 디렉토리는 ctrl+c, ctrl+v 로 다른 프로젝트에 바로 복붙할 수 있다는 것을 기억하시면 됩니다. 반대로 omct 디렉토리는 omct 애플리케이션에서만 사용됩니다.
그래서, 코드의 퀄리티를 높이고 싶다면 omct에 포함된 모듈 보다는 base에 포함된 모듈을 우선순위 있게 작업해야 합니다.
위 처럼 폴더 규칙이 있다면 어떤 로직이 어디에 속하는지 빠르게 파악하여 위에서 말한 비용을 크게 줄일 수 있을 것 입니다.
- controll and uncontrolled component - https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components
- headless component - https://tanstack.com/table/v8/docs/guide/introduction
- 토스 effective component - https://youtu.be/fR8tsJ2r7Eg?si=6orGsoZEiyujEf7g
- colocation - https://kentcdodds.com/blog/colocation