5. 협업규칙 및 컨벤션 - Licruit/Licruit-backend GitHub Wiki
- 전체 회의: 매주 월요일 20:00 PM
- 멘토링: 매주 월요일 20:30 PM
저희 팀은 짧은 주기로 개발과 테스트, 배포를 반복하는 Agile 방법론을 도입하여 개발의 유연성을 제고하였습니다. 특히 Agile 방법론 중에서도 Scrum을 기반으로 하되, 일부 규칙을 우리 팀에 맞게 조정하였습니다.
- 전체 개발 기간을 반복적인 개발 주기인 스프린트(Sprint)로 나누어 개발을 진행합니다.
- 스프린트 단위는 1주로 합니다. (그러나, 상황에 따라 조절 가능합니다.)
- 제품 백로그는 기능 명세서로 대체합니다.
- 매 스프린트 마다 본인이 맡은 기능을 구현하기 위한 task를 task board에작성합니다.
- 매 스프린트 마다 release를 반복합니다.
- 스프린트 목표 달성이 완료되지 않은 경우, 차기 스프린트로 연장할 수 있습니다.
- 정기회의의 주목적은 ‘스프린트 리뷰’입니다.
- 스프린트 결과물에 대한 테스트를 진행합니다.
- 해당 스프린트 동안 개선할 점, 요구사항의 변화, 이슈 사항을 체크하고 공유합니다.
- 스프린트 리뷰 이후 차기 스프린트 계획 회의를 진행합니다.
- 기능 선정 → 조율이 필요한 사항 논의 → task 설정
노션의 task board를 활용하여 스프린트마다 task를 작성하였고, 모든 팀원의 task를 서로 공유했습니다.
- 이슈 생성
본인이 어떤 작업을 할 것인지, 맡은 기능에 대한 세부적인 작업 태스크를 나열합니다.
- 이슈 넘버 기반으로 브랜치 생성
생성된 이슈 넘버를 이용하여 작업 브랜치를 생성합니다. (ex. feat/#1)
- 작업 후 dev 브랜치에 PR
작업이 완료된 후, dev 브랜치에 PR을 올립니다. PR 내용으로 본인이 작업한 내용에 대해 상세하게 설명합니다.
- 코드 리뷰
PR은 모든 팀원이 코드 리뷰를 마친 후 approve를 받아야 합니다.
- 브랜치 병합
작업자 본인은 코드 리뷰를 확인한 후 수정사항/이슈사항 등을 반영한 뒤, 작업 브랜치를 dev 브랜치에 병합합니다.
지금까지 저희 팀원들은 프로젝트 구조를 파일 유형별로 그룹화한 형태로 사용해왔습니다. (pages, components…)
그러나 하나의 페이지에 속하는 수많은 컴포넌트들을 한 폴더(components/pagename) 내에서 관리하다보면 프로젝트 규모가 점차 커졌을 때 점점 어떤 기능과 연관된 코드를 탐색하는 과정이 어려워지고, 이걸 또 다시 하위 디렉토리로 구분하자니 폴더의 depth가 계속해서 깊어져 그다지 좋은 방식이라 생각하지 않았습니다.
따라서 저희는 여러가지 레퍼런스를 찾아보았고 결론적으로 기능 기반(feature-based) 폴더 구조가 지금의 난관을 해결해줄 수 있을 것이라 판단했습니다.
public
│ └─ assets
│ ├─ icons
│ └─ images
src
├─ pages # 페이지
├─ constants # 상수
├─ store # 전역 상태 관리 스토어
├─ styles # 전역 스타일링
├─ types # 전역 타입
├─ features
│ └── Review # 기능
│ ├── components
│ ├── hooks
│ ├── api
│ ├── models
│ ├── utils
│ └── index.ts # export
│
├─ components # 재사용 가능한 공통 컴포넌트
│ ├── Input
│ ├── Button
│ └── Layouts
│
├─ utils # 유틸리티 함수 (자주 사용되는 기능 모듈화)
├─ hooks # 커스텀 훅
├─ routes # router
├─ App.tsx
└─ index.tsx
features 폴더는 기능별 폴더입니다.
이곳에 기능 별로 폴더를 나누고, 그 내부에는 그 기능과 관련된 UI 컴포넌트, 커스텀 훅, 유틸 함수 등을 위치시켰습니다.
기존의 파일 유형 기반 폴더 구조에서는 이 모든 것들이 components, hooks, utils 폴더에 각각 나뉘어 위치하고 있었다면, 현재 저희의 폴더 구조는 기능에 필요한 파일들만 모아 한 곳에 묶어 놓은 형태입니다.
이와 같은 폴더 구조를 통해 저희는 어떠한 기능에 대해서 수정 작업이 필요할 때, 기능에 연관된 요소들이 한 곳에 모여있어 코드를 탐색하는 과정이 현저히 줄어들었습니다.
(fyi. features 폴더와 같은 depth에 위치하는 hooks, utils, components 폴더의 경우, 어떠한 기능에 종속되지 않고 여러 곳에서 공통적으로 사용 가능한 것들이 포함됩니다.)
각 기능별 폴더에는 public API 역할이 되는 index.ts
를 생성했습니다.
이 파일에서는 외부에서 사용해야하는 public 컴포넌트를 기능 폴더에서 외부로 내보내는 역할을 합니다.
export { default as Banner } from './components/Banner';
이 방법을 통한다면 개발자들은 폴더 내부 구조를 자세히 알지 않더라도 컴포넌트를 손쉽게 import 하여 사용할 수 있고, 외부에서 활용되지 않는 컴포넌트들은 기능 폴더 내부에 private화 하여 캡슐화를 구현할 수 있습니다.
또한 import 경로를 훨씬 더 직관적이고 단순하게 만든다는 장점도 있습니다.
// 직접 import
import Banner from '@/features/Main/components/Banner';
// public API를 통한 import
import { Banner } from '@/features/Main';
src
├─ config
├─ routes
├─ validators
├─ controllers
├─ services
├─ dto
├─ utils
├─ errorHandler
├─ models
└─ index.ts
├─ auth.ts
└─ app.ts
- 모든 함수는 가급적이면 Arrow Function으로 작성하고, 선언할 때 export 합니다.
- 함수명은 동사형으로 작성하고, 카멜 케이스를 사용합니다.
- controller에서는 함수명에 add, get, put/change, remove를 사용합니다.
- service에서는 함수명에 SQL 명령어(insert, select, update, delete)를 사용합니다.
- 파일명은 복수형대상.단수형폴더명.ts로 작성합니다.
- routes: users.route.ts
- controllers: users.controller.ts
- services: users.service.ts
- validators: users.validator.ts
- 모든 함수 이름 끝에 Validate를 붙입니다.
- models: users.model.ts
- dto: users.dto.ts
- tests: users.test.ts
- env파일 변수명은 대문자와 언더바(_)를 사용합니다.
-
route
- controller 함수는 Error Handler로 감싸서 작성합니다. controller에서 try, catch 구문을 없애기 위함입니다.
router.post('/login', ..., wrapAsyncController(login));
-
controller
- try, catch를 사용하지 않습니다.
- 예상 가능한 모든 에러는 controller에서 utils/HttpException 함수를 통해 발생시킵니다.
export const addUser = async (req: Request, res: Response) => {
// ...
if (foundUser) {
throw new HttpException(StatusCodes.BAD_REQUEST, '이미 사용된 사업자번호입니다.');
}
// ...
return res.status(StatusCodes.CREATED).end();
};
-
service
- try, catch 구문을 사용합니다.
- catch에서 기능에 대한 에러 메시지를 작성합니다.
- express-validator 라이브러리를 사용하여 유효성 검사를 합니다.
- validators/validate 파일에 있는 validate 함수는 유효성 검사 결과를 확인하여 400 응답을 보내는 역할을 하므로 이 파일은 건들지 말아야 합니다.
- 유효성 검사 필드가 2개 이상일 경우, 배열로 작성하여 export 합니다.
import { body } from 'express-validator';
// 각 필드에 대한 유효성 검사 함수 작성
export const contactValidate = body('contact').notEmpty().isString().withMessage('연락처 확인 필요');
export const companyNumberValidate = body('companyNumber')
.notEmpty()
.isString()
.isLength({ min: 10, max: 10 })
.withMessage('사업자번호 확인 필요');
// 해당 route에 대해 유효성 검사 항목이 2개 이상일 경우 배열로 export
export const resetPwValidate = [companyNumberValidate, contactValidate];
- 작성한 유효성 검사 함수는 router의 미들웨어로 넣습니다. 마지막에 validate 함수도 함께 작성해야 유효성 검사가 정상적으로 이루어집니다.
router.post('/register', [...registerValidate, validate], wrapAsyncController(addUser));