코드 컨벤션의 중요성
동아리에서 약 1년 ~ 1년 반에 걸친 길고 길었던 쩝쩝박사 프로젝트가 마무리 되었습니다. 회고를 진행하면서 FE 팀원 공통적으로 얘기했던 코드 컨벤션에 대해 이야기해보려 합니다.
시작하기 전…
협업을 진행하면서 코드 컨벤션을 구체적으로 정하지 않고 진행했을 때 겪었던 문제점 및 해결과정 그리고 제안할점에 대해 쓴 글입니다.
코드 컨벤션이란?
- 읽고 관리하기 쉬운 코드를 작성하기 위한 일종의 코딩 스타일 규약(하나의 작성 표준)이다.
- 특히, 다른 언어에 비해 유연한 문법 구조를 가진 언어일수록 (ex. JavaScript) 개발자 간 통일된 규약이 없다면 코드의 의도를 파악하거나 오류를 찾기 어려우며 유지보수 비용이 늘어나기 때문에 코드의 가독성을 높이고 작성한 코드를 효율적으로 유지보수하기 위해서는 공통의 규칙 (코드 컨벤션)을 꼭 작성할 필요가 있다.
위의 설명과 같이 통일된 규약이 없다면 코드의 의도나 오류를 찾기 어렵기 때문에 꼭 필요한 단계라 정의되어 있습니다. 특히 협업에서는 꼭 필요한 단계입니다.
하지만, 그 당시에 처음으로 진행해보는 협업 프로젝트였고, 처음 적용해 보는 기술 스택(Typescript, TanStack Query, Sass 등)을 학습하기 벅찼기에 코드 컨벤션에 대해 깊게 고려하지 못했어요. (이게 큰 스노우볼이 될 줄이야…)
언제 깨달았어?
처음이 두 번이 되고, 세 번이 될 때쯤엔…이미 늦어버렸다.
처음. 폴더 구조
pr 과정에서 다양한 폴더 구조를 볼 수 있었습니다. 아래는 기억에 남는 폴더 구조예요.
-
함수명 폴더 구조
이 당시에는 복잡한 폴더 구조를 가질 경우에만 함수명 폴더로 분리해 놓는 것을 권장했어요. 대부분의 폴더 구조가 components 폴더 내에 함수명 폴더로 이루어졌기에 권장했던 방식이었죠.
폴더 구조 Before/After
// Before
📦Inquiry
┣ 📂hooks
┣ 📂Inquire
┃ ┣ 📂components
┃ ┃ ┣ 📂Explain
┃ ┃ ┗ 📂InquireForm
┃ ┃ ┃ ┣ 📂components
┃ ┃ ┃ ┃ ┗ 📂RequiredLabel
┗ 📂Inquiry
┃ ┣ 📂components
┃ ┃ ┣ 📂InquiryList
┃ ┃ ┃ ┣ 📂components
┃ ┃ ┃ ┃ ┗ 📂InquiryBlock
┃ ┃ ┃ ┃ ┃ ┣ 📂components
┃ ┃ ┃ ┃ ┃ ┃ ┗ 📂Answer
┃ ┃ ┣ 📂InquirySelectButton
┃ ┃ ┗ 📂SearchBar// After
📦Inquiry
┣ 📂hooks
┣ 📂Inquire
┃ ┣ 📂components
┃ ┃ ┣ 📂Explain
┃ ┃ ┗ 📂InquireForm
┃ ┃ ┃ ┣ 📂RequiredLabel
┗ 📂Inquiry
┃ ┣ 📂components
┃ ┃ ┣ 📂InquiryList
┃ ┃ ┃ ┣ 📂InquiryBlock
┃ ┃ ┃ ┃ ┣ 📂Answer
┃ ┃ ┃ ┃ ┣ 📂InquiryImages
┃ ┃ ┣ 📂InquirySelectButton
┃ ┃ ┗ 📂SearchBar
-
Mobile/PC 분리
Mobile 환경과 PC 환경을 분리해서 기능을 구현한 팀원도 있었어요. 각각의 환경에서 디자인이 너무 달랐기에 서로 분리해서 진행한 경우가 이에 해당해요.
📦Setting
┣ 📂hooks
┣ 📂Mobile
┣ 📂PC
┣ 📂static
┣ 📂Withdrawal
┗ 📜index.tsx
이 외에도 다양한 폴더 구조가 있었지만 폴더 구조에 대한 정답은 없고, 그저 복잡하지 않다면 괜찮다고 생각했기 때문에 크게 불편함을 느끼지 않았어요. 무엇보다 기능 구현이 가장 큰 우선순위에 있기에 폴더 구조에 대해서는 크게 고려하지 않았죠.
두번. 혼용하는 네이밍들
주간 공유 시간에 나온 혼용하는 네이밍들에 관해서 정리해야할 상황이었습니다.
- scss를 import하는 과정에서 style이나 styles로 혼용해서 사용
- 상점을 store이나 shop으로 혼용해서 사용
위의 두 문제 사항에 대해 처음 든 생각은 ‘그럼 다음 pr부터 고치면 되겠다’였어요.
간단하게, styles로 import하는 비율이 많기 때문에 styles로, 전역상태를 store 폴더에 관리하기 때문에 상점은 shop으로 통일하는 것으로 결정했죠.
사소한 네이밍 이슈라 생각했지, 코드 컨벤션을 정하지 않았기에 발생한 문제점이라고는 생각지도 못했거든요.
세번. QA에서 발견한 다양한 네이밍
배포 전 QA를 진행하면서 다양한 이슈가 발생했고, 자신이 구현한 부분에서의 이슈 해결뿐만이 아니라 다른 사람이 구현 부분에서도 해결이 필요한 상황이었어요.
그렇다 보니 다른 사람이 작성한 코드 스타일과 내가 작성한 코드 스타일이 정말 다르다는 것을 깨닫게 되었습니다. 그제야 코드 컨벤션의 중요성을 깨달았죠. 위에서 안일하게 생각했던 문제점들의 총집합이었습니다.
-
interface 명
아래 예시는 GET api의 응답 데이터를 interface 타입으로 나타냈을 때의 예시입니다. 각각 상점/리뷰/팔로워 기능을 맡았던 사람이 달랐기에 이에 따른 interface 명명 규칙도 서로 달랐어요.
// 단일 상점 조회 : Fetch + 타입명 + Response
export interface FetchShopResponse {
// ...
}
// 작성한 리뷰가 있는 상점 리스트 조회 : 타입명 + Response
export interface ReviewedShopsResponse {
// ...
}
// 팔로워 목록 조회 : Get + 타입명 + Response
export interface GetFollowListResponse {
// ...
} -
api 요청함수명
위의 응답 데이터의 interface 명이 다른 것 처럼 api 요청함수명 또한 서로 달랐습니다. fetch와 get을 구분 없이 사용하는 함수명이 많았었죠.
// fetch + 함수명
export const fetchShop = async (placeId: string) => {
const { data } = await shopApi.get<FetchShopResponse>(`/shops/${placeId}`);
return data;
};
// get + 함수명
export const getReviewedShops = async () => myPageApi.get<ReviewedShopsResponse>('/review/shops?size=10');
// 함수명
export const followList = (pageParam: string) => followApi.get<GetFollowListResponse>(`/follow/followers?pageSize=10&${pageParam}`); -
컴포넌트 props명
컴포넌트의 Props 역시
Props
만을 사용하거나,컴포넌트명 + Props
를 혼용해서 사용하고 있었어요.// Type1 : Props
interface Props {
className: string;
onClick: () => void;
isActive: boolean;
}
// Type2 : 컴포넌트명 + Props
interface SearchBarProps {
className?: string;
onChange: (text: string) => void;
onSubmit: () => void;
}
이 외에도 파일명, 변수명 등 다양한 부분에서 코드의 일관성을 느낄 수가 없었어요. 다른 사람이 작성한 코드를 이해하는 것도 벅찼는데 코드의 일관성이 없으니 유지보수 및 가독성 측면에서 떨어지는 것이 확연히 느꼈죠.
제안할 점
위와 같은 문제점을 겪었기에 프로젝트 진행전 코드 컨벤션을 진행할 때 몇 가지 사항을 제안합니다.
1. 구체적인 네이밍 정하기
구글에 검색을 하면 다양한 코드 컨벤션의 예시가 있습니다. 제안한 네이밍의 경우 이번 프로젝트를 진행하면 놓쳤던 네이밍 컨벤션이기 때문에 아래의 예시를 무조건 따르기보단 각 상황에 맞게 적용하면 될 것 같아요.
-
interface 명/api 요청함수명
쩝쩝박사 프로젝트에서 api 폴더 내의 interface 명은
api의 응답 데이터 타입
을 나타내는 interface와 api 요청에 필요한매개변수 타입
을 타나내는 interface로 나뉠 수 있어요.이때, 응답의 경우는
타입명 + Response
으로, 매개변수는타입명 + Parmas
형식으로 진행했을 때 코드가 더 이해하기 편했어요. 이 외에 Fetch나 Get을 접두사로 놓는 경우가 있었지만, interface 명이 너무 길면 오히려 가독성이 떨어진다는 느낌이 들었어요.api 요청함수는 HTTP 메서드 종류를 접두사로 사용했던 게 더 직관적이었어요. 예를 들어, GET 요청 함수라면
get + 함수명
의 형식으로 작성하는 것처럼요.위의 사항을 토대로 기존의 코드를 변경하면 아래와 같이 변경할 수 있어요.
// 단일 상점 조회 : 타입명 + Response
export interface ShopResponse {
// ...
}
// get + 함수명
export const getShop = async (placeId: string) => {
const { data } = await shopApi.get<ShopResponse>(`/shops/${placeId}`);
return data;
}; -
컴포넌트 props명
컴포넌트의 props의 경우
컴포넌트+Props
으로 네이밍 하지 않아도 명확하게 컴포넌트에서 선언되기 때문에Props
타입으로 지정해도 괜찮아요. 통일성 없는 타입들을 아래와 같이 변경할 수 있겠네요.// Props로 통일
interface Props {
className: string;
onClick: () => void;
isActive: boolean;
}
interface Props {
className?: string;
onChange: (text: string) => void;
onSubmit: () => void;
}
2. 폴더 구조 통일
개인적인 생각이지만 폴더 구조는 함수명 폴더로 분리