팀 프로젝트, 회사 등등...
개발적인 약속 즉, 컨벤션을 지키는 것은 협업에서 중요하다. 클린 코드 작성도 컨벤션의 일종이라고 볼 수 있고 이를 바탕으로 코드를 짜는 것은 매우 중요한 일이다.'
개인 프로젝트에서는 어떤식으로 코드를 짜도 내가 짠 코드이기 때문에 언제든지 해당 코드를 알아볼 수 있다. 하지만 코드를 짜는 스타일은 개개인마다 다를 수 밖에 없고, 여러 사람들의 코드가 들어갈 시 클린 코드를 바탕으로 코드를 짜지 않으면 다른 사람의 코드가 들어간 부분을 건드리게될 상황이 오면 매우 당혹스럽고 효율도 떨어질 것이다.
어떤 식으로 클린 코드 짜는 법을 연습할 수 있을까 고민하던 와중, 토스 개발 블로그에서 매우 좋은 자료를 발견하였다.
참고: https://toss.im/slash-21/sessions/3-3
해당 자료를 참고하여 내가 짠 코드를 클린 코드로 리팩토링 과정을 소개하려 한다.
응집도
토스 블로그에서는 같은 목적의 코드는 뭉쳐두는 것이 좋다고 설명하고 있고, 이것이 지켜진 코드를 응집도가 좋은 코드라고 한다.
현재 내가 진행하고 있는 프로젝트에서 장소를 큐레이션 안에 스크랩하여 넣을 수 있는 기능을 응집도를 바탕으로 리팩토링을 해보려고 한다.
위에서 빨간색으로 표시된 스크랩 버튼을 누르면
위와 같이 toast와 함께 현재까지 만들어놓은 큐레이션이 뜸과 동시에 새 큐레이션 만들기 버튼을 가지고 있는 모달창이 뜨게 된다.
새 큐레이션 만들기 버튼을 누르면 위와 같이 큐레이션을 생성할 수 있는 추가 모달이 화면에 뜨게된다.
우선 해당 기능들과 관련된 코드들을 단순히 기능 구현만을 위해 짜보았다.
...
export default function MyCurationModal({
open,
title,
myCurationData,
spaceId,
handleModalFn,
}: MyCurationModalProps) {
//새 큐레이션 모달 관련 코드
const [openMakeCurationModal, setOpenMakeCurationModal] = useState(false);
//새 큐레이션 모달 관련 코드
const handleMakeCurationClick = () => {
setOpenMakeCurationModal(true);
};
...
<div
className="flex items-center gap-[1.2rem] mt-[2rem] mb-[1.2rem]"
onClick={handleMakeCurationClick}
>
//새 큐레이션 모달 관련 코드
<div className="w-[6rem] h-[6rem] bg-background-gray-2 rounded-lg flex p-[2.4rem]">
<AddIcon />
</div>
<span className="body1-medium text-text-gray-8">
새 큐레이션 만들기
</span>
</div>
...
<Suspense
fallback={
<UseDeferredComponent>
<div className="w-full h-[6rem] bg-background-gray-2 animate-pulse" />
</UseDeferredComponent>
}
>
<div className="flex flex-col items-start gap-[0.8rem]">
{myCurationData?.curation.map((curationData) => (
<MyCurationCard
key={curationData.id}
curationData={curationData}
spaceId={spaceId}
/>
))}
</div>
</Suspense>
...
//새 큐레이션 모달 관련 코드
<CurationMakeModal
isOpen={openMakeCurationModal}
handleOpen={setOpenMakeCurationModal}
/>
...
위 코드는 스크랩 버튼을 누르면 나오는 모달 컴포넌트 파일에 있는 코드이다.
위처럼 코드를 짜면 기능에는 아무 문제가 없다. 그럼 뭐가 문제일까?
우선 새 큐레이션 만들기 모달을 띄우기 위해 useState로 변수 상태를 관리하고 있는데, 주석으로 "새 큐레이션 모달 관련 코드" 로 표시 된 부분만 4군데이다.
즉, 하나의 기능을 파악하기 위해 내가 아닌 다른 사람은 남의 코드를 4군데나 스크롤하며 파악해야한다는 것이다. 이는 응집도에 좋지 않은 코드이다.
또한 해당 모달에서 큐레이션 카드를 클릭하면 클릭한 스크랩 버튼의 장소가 해당 큐레이션에 들어가며 toast 가 나타나게 되는데, 이는 이 모달에서 매우 핵심적인 기능이다.
근데 나는 이 액션들을 MyCurationCard 컴포넌트에 직접 구현을 한 상태이고, 남이 이 컴포넌트의 코드를 보면 카드를 클릭하면 뭔 일이 일어나는지 다른 컴포넌트로 이동을 해야지만 확인할 수 있다. 이도 코드를 파악하는데에 좋지 않다.
TIP: 핵심 데이터는 밖에서 전달, 나머지는 뭉쳐라!
토스 블로그에서는 응집도를 높이기 위한 팁으로 같은 기능은 뭉쳐두는 것이 중요하다고 한다.
그러나 단순히 뭉치기에만 집중한다면 위처럼 핵심 기능들이 감춰짐으로써 오히려 혼란을 줄 수도 있다.
따라서 뭉치되 핵심 데이터와 세부 데이터를 나누어서 핵심 데이터는 한눈에 파악할 수 있도록 하는 것이 중요하다.
...
export default function MyCurationModal({
open,
title,
myCurationData,
spaceId,
handleModalFn,
}: MyCurationModalProps) {
const { isModalOpen, openModal, handlers } = useOpenCurationMakeModal();
const { isToastOpen, toastText, openToast } = useToast();
const handleMakeCurationClick = () => {
openModal();
};
const handleMyCurationCardClick = async (curationId: number) => {
const res = await PostSavePlaceAtCuration(curationId, spaceId);
if (res.status === 200) {
openToast("큐레이션에 장소가 추가되었습니다.");
revalidateScrapSpace();
revalidateMyCuration();
revalidatePlaceDetail();
revalidateCurationDetail();
revalidateTextSearchPlaceData();
revalidateKeywordSearchPlaceData();
} else {
alert("오류가 발생했습니다!");
}
};
...
<CurationMakeButton
text="새 큐레이션 만들기"
curationMakeModalInfo={{
open: isModalOpen,
handleModalFn: handlers.handleModal,
}}
onClick={handleMakeCurationClick}
/>
...
<Suspense
fallback={
<UseDeferredComponent>
<div className="w-full h-[6rem] bg-background-gray-2 animate-pulse" />
</UseDeferredComponent>
}
>
<div className="flex flex-col items-start gap-[0.8rem]">
{myCurationData?.curation.map((curationData) => (
<MyCurationCard
key={curationData.id}
curationData={curationData}
toastInfo={{
open: isToastOpen,
text: toastText,
}}
onClick={() => handleMyCurationCardClick(curationData.id)}
/>
))}
</div>
</Suspense>
...
위는 응집도를 고려하여 리팩토링 한 결과이다.
우선 컴포넌트 prop 수정을 통해 응집도를 높였다.
예를 들어 위에서는 4군데로 각각 떨어져 있던 새 큐레이션 만들기 모달 띄우는 기능과 관련된 코드를 CurationMakeButton 컴포넌트를 생성 & prop을 통해 해당 모달 상태를 전달해 준 뒤 해당 컴포넌트 내부에서 모달을 띄우게 함으로써 같은 기능과 관련된 코드가 흩뿌려져 있는 것을 개선하였다..
핵심 기능인 큐레이션을 클릭하면 장소가 해당 큐레이션에 들어가는 액션은 바로 확인할 수 있도록 꺼내놓았고, 해당 액션을 onClick prop을 통해 전달하는 식으로 구현하였다.
이 밖에도 버튼의 '새 큐레이션 만들기' 텍스트를 핵심 데이터로 판단하여 해당 텍스트를 밖에서 전달하였다. 이를 통해 누구나 이 텍스트를 바로 확인한 뒤 무슨 컴포넌트인지 확인이 가능할 수 있을 것이라 판단하였다.
또한 큐레이션 만들기 모달의 열고 닫는 상태 및 로직, toast가 나타나고 사라지는 상태 및 로직은 세부 데이터라 판단하여 Custom hook을 통해 뭉친 뒤 사용하였다.
위와 같이 핵심 데이터는 밖에서 전달, 세부 데이터는 뭉치는 방법을 통해 응집도를 개선할 수 있었다.
단일책임
하나의 함수는 하나의 기능만 수행할 수 있도록 하는 것이 좋다는 단일책임도 강조하고 있다.
예를 들어 위의 코드에서 큐레이션을 클릭했을 때 수행되는 액션 코드를 봐보자.
const handleMyCurationCardClick = async (curationId: number) => {
const res = await PostSavePlaceAtCuration(curationId, spaceId);
if (res.status === 200) {
openToast("큐레이션에 장소가 추가되었습니다.");
revalidateScrapSpace();
revalidateMyCuration();
revalidatePlaceDetail();
revalidateCurationDetail();
revalidateTextSearchPlaceData();
revalidateKeywordSearchPlaceData();
} else {
alert("오류가 발생했습니다!");
}
};
위 함수는
위 영상처럼 POST api 콜을 수행하고, toast를 띄우고, 데이터를 추가했으니 해당 데이터와 연관된 데이터를 업데이트 시키기 위해 Cache를 revalidate 하는 서버 액션까지 수행하고 있다.
위처럼 여러 기능들을 하나의 함수에 우겨넣으면 해당 코드를 다른 사람이 봤을 때 어느 기능들을 수행하고 있는지 한눈에 파악하기가 힘들 것이다.
단일책임 즉, 하나의 함수가 하나의 기능을 할 수 있도록 리팩토링 한다면 각 함수가 해당 기능에 맞게 명확한 이름을 가질 수 있을 것이고, 다른 사람이 볼 때 함수의 이름만 봐도 해당 코드를 파악하기가 수월할 것이다.
const savePlaceAtCuration = async (curationId: number) => {
const res = await PostSavePlaceAtCuration(curationId, spaceId);
return res.status;
};
const revalidateRelatedData = () => {
revalidateScrapSpace();
revalidateMyCuration();
revalidatePlaceDetail();
revalidateCurationDetail();
revalidateTextSearchPlaceData();
revalidateKeywordSearchPlaceData();
};
const handleMyCurationCardClick = async (curationId: number) => {
if ((await addPlaceAtCuration(curationId)) === 200) {
openToast("큐레이션에 장소가 추가되었습니다.");
revalidateRelatedData();
} else {
alert("오류가 발생했습니다!");
}
};
위와 같이 단일책임을 중심으로 리팩토링 한다면, 큐레이션을 클릭했을 때 수행되는 각각의 기능들을 각각의 함수로 생성한 뒤 명확한 이름을 부여해줌으로써 한눈에 어떤 기능들이 일어나는지 파악할 수 있을 것이다.
이는 컴포넌트에서도 마찬가지라고 설명하고 있다.
위는 위에 첨부한 클린코드의 ppt 자료이다.
왼쪽처럼 옵저버를 달고, api 콜도 수행하는 다중 책임의 컴포넌트 보다는, 오른쪽처럼 컴포넌트를 생성하여 옵저버를 다는 로직을 숨기고 밖에서는 api 콜만 신경쓸 수 있도록 정리하는 것이 협업에 좋은 코드일 것이다.
만약 조건이 많아지면 변수 및 함수 이름 등을 한글로 작성하는 것도 나름 좋은 방법이라고 설명하고 있다.
이는 전혀 생각하지 못했었는데 너무 이름이 복잡해지거나 조건이 많아지면 나도 한글 이름으로 짜보기도 해봐야겠다.
추상화
추상화는 말그대로 얼마나 구체적인지를 나타내는 말이다.
자료 PPT에 나와있는 왼쪽처럼 명령형 프로그래밍을 사용하면 추상화와 재활용성이 낮아질 것이고, 오른쪽처럼 선언형 프로그래밍을 사용하면 추상화와 재활용성이 높아질 것이다.
추상화 정도를 정하는 것에는 정답이 없고 때에 따라 적절히 사용하면 된다.
다만,
왼쪽 처럼 추상화의 정도가 뒤죽박죽으로 구현되어 있으면 전체적인 코드의 구체화 정도를 파악하기 힘들고 가독성도 떨어진다.
오른쪽 처럼 비슷한 추상화의 정도를 비슷하게 맞추는 것이 다른 사람들이 코드를 읽기에도 편하고, 전체적인 구체화 정도를 파악하고 추후 기능을 추가하거나 수정하는데 도움이 될 것이다.
클린 코드 !== 짧은 코드
매우 중요한 결론인 것 같다.
나도 코드를 짜며 은연중에 짧은 코드가 무조건 좋은 것이라 생각하고 코드의 길이를 줄이는 것에만 신경을 썼었던 것 같다.
위처럼 응집도, 단일책임을 위해 리팩토링을 하는 과정에서도 코드의 길이는 길어졌지만, 협업에 더 좋은 코드가 되었다.
이 밖에도 클린 코드를 유지하는 방법에는 많은 것들이 있겠지만, 쉽게 생각하지 못한 부분을 좋은 자료를 만나게 되어 내 코드에도 적용해볼 수 있었다.
앞으로 협업에 좋은 코드를 항상 생각하고 큰 그림을 그려가며 코드를 짜는 프론트엔드 개발자가 되기 위해 달려나가야겠다.
'FrontEnd > React' 카테고리의 다른 글
Context API 에서 Recoil로 마이그레이션 일지 (3) | 2024.04.06 |
---|---|
React 에서 Context API 효율적으로 사용하기 (0) | 2024.04.04 |
Suspense 효율적으로 사용하기 (4) | 2024.03.21 |
서비스 내에 나만의 지도 띄우기 (Feat : NAVER MAP) (4) | 2024.02.27 |
React-Transition-Group 으로 애니메이션 주입하기 (0) | 2024.02.18 |