토스 Frontend Fundamentals - 코드 퀄리티
0. 프론트엔드 코드 퀄리티를 다시 생각하며 – 토스 문서와 내 경험
올해 초, 토스에서 발행한 Toss Frontend Fundamentals 문서 중 '코드 퀄리티' 부문을 읽었다. 실제 프로젝트에서 코드를 구현하며 생긴 고민들과 겹치는 부분이 많아, 내 경험을 기반으로 내용을 정리하고 실제 코드에 적용 가능한 방안을 살펴보고자 한다.
좋은 코드를 위한 4가지 기준
문서에서는 좋은 코드를 판단하는 기준을 다음 네 가지로 정의한다.
- 가독성
- 예측 가능성
- 응집도
- 결합도
내 경험상 가장 와닿는 부분은 가독성과 응집도, 결합도였다. 실제로 나는 구현 위주로 코드를 작성하다 보니, 한 컴포넌트 안에 여러 책임과 로직이 뒤섞여 있는 경우가 많았다.
예제 코드들은 토스 Frontend Fundamentals 문서에서 가져온 것이다.
1. 가독성: 맥락 줄이기, 구현 상세 추상화하기
토스 문서에서는 컴포넌트가 읽기 어려운 이유 중 하나로 맥락이 많다는 점을 꼽는다. 예를 들어, 버튼 클릭 이벤트에 여러 비동기 로직과 UI 상태 관리까지 한꺼번에 작성하면, 한눈에 역할을 이해하기 어렵다.
나도 가장 자주 사용했던 코드 팬턴으로, 버튼 컴포넌트 안에 클릭 핸들러 하나에 모든 로직을 집어넣는 것이었다. 예를 들어 아래와 같은 패턴이다.
function FriendInvitation() {
const { data } = useQuery();
const handleClick = async () => {
const canInvite = await overlay.openAsync(({ close }) => (
<ConfirmDialog
title={`${data.name}님에게 공유`}
cancelButton={
<ConfirmDialog.CancelButton onClick={() => close(false)}>
닫기
</ConfirmDialog.CancelButton>
}
confirmButton={
<ConfirmDialog.ConfirmButton onClick={() => close(true)}>
확인
</ConfirmDialog.ConfirmButton>
}
/>
));
if (canInvite) {
await sendPush();
}
};
return <Button onClick={handleClick}>초대하기</Button>;
}
코드를 보면 버튼 클릭과 사용자 확인, 실제 전송 로직까지 한 컴포넌트 안에 들어 있어 읽기가 어렵다.
이를 개선하기 위해 토스 문서에서는 UI와 로직을 작은 컴포넌트로 추상화하는 방법을 제안한다.
function FriendInvitation() {
const { data } = useQuery();
return <InviteButton name={data.name} />;
}
function InviteButton({ name }) {
const handleInvite = async () => {
const canInvite = await overlay.openAsync(({ close }) => (
<ConfirmDialog
title={`${name}님에게 공유`}
cancelButton={
<ConfirmDialog.CancelButton onClick={() => close(false)}>
닫기
</ConfirmDialog.CancelButton>
}
confirmButton={
<ConfirmDialog.ConfirmButton onClick={() => close(true)}>
확인
</ConfirmDialog.ConfirmButton>
}
/>
));
if (canInvite) await sendPush();
};
return <Button onClick={handleInvite}>초대하기</Button>;
}
이 방식은 한 컴포넌트가 담당하는 맥락을 줄여 읽기와 이해를 쉽게 한다. 또한 버튼과 클릭 후 동작이 가까이에 있어, 수정 시 놓칠 가능성을 줄인다.
2. 응집도: 함께 수정되는 파일은 같은 디렉토리에 두기
응집도는 코드가 서로 관련 있는 파일이나 로직끼리 모여 있는지를 의미한다.
예를 들어, 특정 UI와 그 UI에 필요한 헬퍼 함수, API 호출 모듈이 서로 다른 디렉토리에 흩어져 있으면
-> 수정할 때 참조 경로를 따라가야 하고
-> 의도치 않은 버그가 발생할 가능성이 있다.
나 또한 그동한 개발했던 경험에 따르면, 버튼 클릭 이벤트에 따른 API 호출과 확인 UI가 각각 다른 디렉토리에 있어 작은 UI 수정에도 여러 파일을 찾아야 하는 경우가 많았다. 이를 개선하기 위해 관련 코드끼리 하나의 디렉토리 또는 컴포넌트 내부로 묶어 관리하면, 수정 범위가 명확해지고 유지보수가 쉬워진다.
아래는 기존 폴더 구조와 개선된 폴더 구조를 bash 트리 형태로 예시를 들어 작성한 것이다.
기존 폴더 구조 (응집도가 낮은 경우)
버튼 UI, 관련 API, 다이얼로그 컴포넌트가 서로 다른 디렉토리에 흩어져 있는 경우이다.
src/
├── api/
│ └── friendApi.ts # 친구 초대 API
├── components/
│ └── Button.tsx # 공용 버튼 컴포넌트
├── dialogs/
│ └── ConfirmDialog.tsx # 확인 다이얼로그 컴포넌트
└── pages/
└── FriendInvitation.tsx # 초대 페이지
문제점
FriendInvitation
기능을 수정하려면 api, dialogs, pages 디렉토리를 오가야 한다.- 관련 코드가 흩어져 있어 변경 범위 파악이 어렵다.
개선된 폴더 구조 (응집도를 높인 경우)
친구 초대와 관련된 UI, API, 상태를 하나의 폴더에서 관리한다.
src/
└── features/
└── friend-invitation/
├── api/
│ └── friendApi.ts # 초대 관련 API
├── components/
│ ├── InviteButton.tsx # 초대 버튼
│ └── ConfirmDialog.tsx # 확인 다이얼로그
├── hooks/
│ └── useInvite.ts # 초대 로직 훅
└── FriendInvitation.tsx # 페이지 컴포넌트
개선점
- 관련 파일을 하나의 기능(도메인) 단위로 그룹화했다.
- 수정할 때 해당 폴더에서만 작업하면 된다.
- 기능 단위로 코드 소유권과 책임이 명확해진다.
3. 결합도: 책임을 하나씩 관리하기
결합도가 높으면, 하나의 변경이 여러 곳에 영향을 미친다. 하나의 상태를 여러 컴포넌트가 공유하면서 이벤트 흐름이 복잡해지고, 버그를 찾는 데 시간이 오래 걸린 경우가 많았다.
토스 문서에서는 상태를 공유할 때에도 책임 단위를 명확히 분리할 것을 권장한다. 예를 들어, 페이지 단위 상태와 컴포넌트 단위 상태를 명확히 나누어, 변경이 필요한 경우 최소 범위에서만 수정하도록 한다.
// 페이지 단위 상태
function FriendInvitationPage() {
const [friends, setFriends] = useState([]);
return <FriendList friends={friends} />;
}
// 컴포넌트 단위 상태
function FriendList({ friends }) {
return friends.map((friend) => (
<InviteButton key={friend.id} name={friend.name} />
));
}
이렇게 하면 각 컴포넌트가 자신의 책임만 관리하므로, 코드 이해와 유지보수가 용이해진다.
마치며
Toss Frontend Fundamentals 문서를 읽으면서, 평소 내가 자주 쓰던 핸들러 안에 모든 로직을 몰아넣는 습관과 관련 없는 파일이 흩어져 있는 구조가 코드 가독성과 유지보수에 부정적인 영향을 준다는 사실을 다시금 깨달았고, 이를 개선하기 위한 구체적인 방법들도 배울 수 있었다.
앞으로는
- 컴포넌트 단위로 맥락을 최소화하기
- 관련 로직은 한 곳에 모으기
- 책임 단위를 명확히 관리
하는 것을 실천하려 한다. 이러한 습관을 통해 장기적으로 클린 코드를 자연스럽게 녹아낼 수 있도록 노력하자,,