Skip to main content

코드 컨벤션의 중요성

· 19 min read

동아리에서 약 1년 ~ 1년 반에 걸친 길고 길었던 쩝쩝박사 프로젝트가 마무리 되었습니다. 회고를 진행하면서 FE 팀원 공통적으로 얘기했던 코드 컨벤션에 대해 이야기해보려 합니다.

시작하기 전…

협업을 진행하면서 코드 컨벤션을 구체적으로 정하지 않고 진행했을 때 겪었던 문제점 및 해결과정 그리고 제안할점에 대해 쓴 글입니다.

코드 컨벤션이란?

코드 컨벤션
  • 읽고 관리하기 쉬운 코드를 작성하기 위한 일종의 코딩 스타일 규약(하나의 작성 표준)이다.
  • 특히, 다른 언어에 비해 유연한 문법 구조를 가진 언어일수록 (ex. JavaScript) 개발자 간 통일된 규약이 없다면 코드의 의도를 파악하거나 오류를 찾기 어려우며 유지보수 비용이 늘어나기 때문에 코드의 가독성을 높이고 작성한 코드를 효율적으로 유지보수하기 위해서는 공통의 규칙 (코드 컨벤션)을 꼭 작성할 필요가 있다.

위의 설명과 같이 통일된 규약이 없다면 코드의 의도나 오류를 찾기 어렵기 때문에 꼭 필요한 단계라 정의되어 있습니다. 특히 협업에서는 꼭 필요한 단계입니다.

하지만, 그 당시에 처음으로 진행해보는 협업 프로젝트였고, 처음 적용해 보는 기술 스택(Typescript, TanStack Query, Sass 등)을 학습하기 벅찼기에 코드 컨벤션에 대해 깊게 고려하지 못했어요. (이게 큰 스노우볼이 될 줄이야…)

언제 깨달았어?

처음이 두 번이 되고, 세 번이 될 때쯤엔…이미 늦어버렸다.

처음. 폴더 구조

pr 과정에서 다양한 폴더 구조를 볼 수 있었습니다. 아래는 기억에 남는 폴더 구조예요.

  1. 함수명 폴더 구조

    이 당시에는 복잡한 폴더 구조를 가질 경우에만 함수명 폴더로 분리해 놓는 것을 권장했어요. 대부분의 폴더 구조가 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


  2. Mobile/PC 분리

    Mobile 환경과 PC 환경을 분리해서 기능을 구현한 팀원도 있었어요. 각각의 환경에서 디자인이 너무 달랐기에 서로 분리해서 진행한 경우가 이에 해당해요.

    📦Setting
    ┣ 📂hooks
    ┣ 📂Mobile
    ┣ 📂PC
    ┣ 📂static
    ┣ 📂Withdrawal
    ┗ 📜index.tsx

이 외에도 다양한 폴더 구조가 있었지만 폴더 구조에 대한 정답은 없고, 그저 복잡하지 않다면 괜찮다고 생각했기 때문에 크게 불편함을 느끼지 않았어요. 무엇보다 기능 구현이 가장 큰 우선순위에 있기에 폴더 구조에 대해서는 크게 고려하지 않았죠.

두번. 혼용하는 네이밍들

주간 공유 시간에 나온 혼용하는 네이밍들에 관해서 정리해야할 상황이었습니다.

  1. scss를 import하는 과정에서 style이나 styles로 혼용해서 사용
  2. 상점을 store이나 shop으로 혼용해서 사용

위의 두 문제 사항에 대해 처음 든 생각은 ‘그럼 다음 pr부터 고치면 되겠다’였어요.

간단하게, styles로 import하는 비율이 많기 때문에 styles로, 전역상태를 store 폴더에 관리하기 때문에 상점은 shop으로 통일하는 것으로 결정했죠.

사소한 네이밍 이슈라 생각했지, 코드 컨벤션을 정하지 않았기에 발생한 문제점이라고는 생각지도 못했거든요.

세번. QA에서 발견한 다양한 네이밍

배포 전 QA를 진행하면서 다양한 이슈가 발생했고, 자신이 구현한 부분에서의 이슈 해결뿐만이 아니라 다른 사람이 구현 부분에서도 해결이 필요한 상황이었어요.

그렇다 보니 다른 사람이 작성한 코드 스타일과 내가 작성한 코드 스타일이 정말 다르다는 것을 깨닫게 되었습니다. 그제야 코드 컨벤션의 중요성을 깨달았죠. 위에서 안일하게 생각했던 문제점들의 총집합이었습니다.

  1. interface 명

    아래 예시는 GET api의 응답 데이터를 interface 타입으로 나타냈을 때의 예시입니다. 각각 상점/리뷰/팔로워 기능을 맡았던 사람이 달랐기에 이에 따른 interface 명명 규칙도 서로 달랐어요.

    // 단일 상점 조회 : Fetch + 타입명 + Response
    export interface FetchShopResponse {
    // ...
    }

    // 작성한 리뷰가 있는 상점 리스트 조회 : 타입명 + Response
    export interface ReviewedShopsResponse {
    // ...
    }

    // 팔로워 목록 조회 : Get + 타입명 + Response
    export interface GetFollowListResponse {
    // ...
    }
  2. 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}`);
  3. 컴포넌트 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. 구체적인 네이밍 정하기

구글에 검색을 하면 다양한 코드 컨벤션의 예시가 있습니다. 제안한 네이밍의 경우 이번 프로젝트를 진행하면 놓쳤던 네이밍 컨벤션이기 때문에 아래의 예시를 무조건 따르기보단 각 상황에 맞게 적용하면 될 것 같아요.

  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;
    };
  2. 컴포넌트 props명

    컴포넌트의 props의 경우 컴포넌트+Props으로 네이밍 하지 않아도 명확하게 컴포넌트에서 선언되기 때문에 Props타입으로 지정해도 괜찮아요. 통일성 없는 타입들을 아래와 같이 변경할 수 있겠네요.

    // Props로 통일
    interface Props {
    className: string;
    onClick: () => void;
    isActive: boolean;
    }

    interface Props {
    className?: string;
    onChange: (text: string) => void;
    onSubmit: () => void;
    }

2. 폴더 구조 통일

개인적인 생각이지만 폴더 구조는 함수명 폴더로 분리했던 구조가 더 깔끔하고 보기 편했어요. components 폴더 내에 함수명 폴더로 구분해 놓으니 구분 없이 components에 넣는 것에 비해서 어떤 식으로 컴포넌트를 분리했는지 확연히 눈에 보이더라고요.

  • 이미지
  • 이미지

물론 Mobile/PC로 분리해 놓은 팀원도 있었지만, 각각의 컴포넌트가 명확하게 분리되지 않는다는 문제점이 있기 때문에 저는 위의 방식을 자주 사용했어요. 아래의 예시는 모호한 컴포넌트가 생겼을 때의 예시예요.

import PasswordSuccessModal from 'pages/Setting/PC/PasswordSuccessModal';
// PC 컴포넌트 import

export default function IdChange(): JSX.Element {
// Mobile 폴더 내의 IdChange 컴포넌트 코드...
}

Mobile 폴더IdChange.tsx 컴포넌트를 보면 PC 폴더의 컴포넌트를 import한다는 것을 알 수 있어요. 명확하게 분리하기 어려울 뿐만이 아니라 UI가 변경될 경우 더 모호해지는 상황이 될 수 있기 때문에 좋은 폴더 구조가 아니라는 생각이 들었죠.

폴더 구조는 정말 다양한 방식으로 구성할 수 있지만 협업을 위해서라면 통일하는 것도 고려해 볼 사항이라고 생각해요.

3. import/order 규칙 설정

  • 이미지
  • 이미지

음식점 검색/게시물 검색 부분을 수정했을 때 올라왔던 pr이에요. 확인 결과 각자 맡은 부분에서 import 순서가 다르다는 것을 발견했고, import/ordersort-imports을 활용해 import 규칙을 정했어요.

import/order 규칙
  "import/order": [
"error",{
"groups": ["builtin", "external", "internal", ["sibling", "parent", "index"], "type", "unknown"],
"pathGroups": [
{
"pattern": "react*",
"group": "builtin",
"position": "after"
}
],
"alphabetize": {
"order": "asc",
"caseInsensitive": true
},
"newlines-between":"always",
"pathGroupsExcludedImportTypes": ["react*"]
}
],
  • react가 포함된 import가 가장 최상단에 위치하게 합니다.
  • 알파벳을 기준으로 오름차순으로 정렬됩니다.
  • 그룹사이에 최소 한 줄 이상의 줄 바꿈을 강제합니다.
sort-imports 규칙
      "sort-imports": [
"error",
{
"ignoreCase": true,
"ignoreMemberSort": false,
"ignoreDeclarationSort": true,
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
}
],
  • 모듈의 대소문자 구분을 무시합니다.
  • 모듈 내의 정렬을 무시합니다.
  • import/ordergroups 옵션의 우선순위를 위해 ignoreDeclarationSort 를 true로 변경합니다. 이는 memberSyntaxSortOrder의 규칙에도 영향을 미치지 않습니다.

// Before
import { Link, useLocation } from 'react-router-dom';
import cn from 'utils/ts/classNames';
import { useAuth } from 'store/auth';
import styles from './BottomNavigation.module.scss';
import SpriteSvg from '../SpriteSvg';

//After
import { Link, useLocation } from 'react-router-dom';

import SpriteSvg from 'components/common/SpriteSvg';
import { useAuth } from 'store/auth';
import cn from 'utils/ts/classNames';

import styles from './BottomNavigation.module.scss';

styles를 제외한 모든 경로는 절대경로로 수정했고, 그 결과 위와 같이 정렬된 것을 확인할 수 이었어요. 이전의 뒤죽박죽이었던 import 관련 코드들과 비교해 보았을 때 확실히 import의 흐름이 눈에 더 잘 들어오지 않나요? 이 때문에 프로젝트 시작 전 import/order 규칙을 정해보는 것을 추천해요.

4. 공용 유틸의 주석처리를 통한 접근성 개선

프로젝트 진행 중간에 새로운 팀원들이 들어온 적이 있었어요. 약 2주간의 온보딩 기간을 거친 후에 프로젝트에 참여하게 되었는데 지금 생각해 보면, 새로운 팀원들은 현 프로젝트에 사용되는 기술 스택을 학습하기에 바쁠 뿐만이 아니라 기존에 만들어진 코드를 해석하기도 벅찰 거라는 생각을 못 했던 것 같아요.

특히 이번 프로젝트에는 공용 유틸이 꽤 많았었는데 주석을 통해 유틸에 대한 설명이 있었더라면 원할한 협업에 도움이 되지 않았을까 싶네요.

가장 자주 사용했던 useBooleanState을 예로 들면 주석간략한 설명, params, return에 정보를 기재하는 것이에요. 관련 유틸의 쓰임새를 알 수 있기 때문에 코드를 이해하는 데 도움이 될 것 같다는 생각이 들지 않나요? 다음 프로젝트에서는 중간에 투입될 새로운 팀원을 위해 주석을 통해 설명을 곁들여보는 것을 추천드려요.

import {
Dispatch, SetStateAction, useCallback, useState,
} from 'react';

type ReturnType = [boolean, () => void, () => void, () => void, Dispatch<SetStateAction<boolean>>];

/**
* boolean 상태를 다루는 커스텀 훅
* @param {boolean} defaultValue 초기값
* @return [value, setTrue, setFalse, toggle, setValue]
*/
export default function useBooleanState(defaultValue?: boolean): ReturnType {
const [value, setValue] = useState(!!defaultValue);

const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
const toggle = useCallback(() => setValue((x) => !x), []);

return [value, setTrue, setFalse, toggle, setValue];
}

이미지

마무리

코드 컨벤션을 구체적으로 하지 않았을 때의 다양한 문제점을 겪었지만, 첫 프로젝트였던 만큼 완성했다는 것에 의의를 두고 싶네요. 소통이나 기능 개발에 우선순위를 뒀지, 코드 컨벤션에 대해서는 깊게 고민해 본 적은 없었는데…글로 정리하면서 생각 외로 놓친 부분이 많았다는 것을 느꼈어요. 어쩌면 여전히 놓친 부분이 있을 수도 있고요. 그렇기 때문에 다음 프로젝트 때에는 좋은 코드 컨벤션으로 원활한 협업을 진행해 봐요! 마지막으로 이번 쩝쩝박사 프로젝트에 참여한 모든 분들 수고 많으셨습니다!