"지윤님 여기에 처음으로 돌아가는 버튼을 추가해 주세요."
"지윤님 이거 동작이 이상해요!"
"지윤님 요 모달에-"
코드는 끝이 없다. 시간이 흘러서 버그이든 기능의 추가든 수정이든 사소한 문구 변화든 어떠한 이유로든 나는 내가 짠 코드를 다시 마주하게 된다. 엉망으로 짠 코드에는 간단한 기능 하나 끼워넣기도 까다로워 부담스러워진다. 다시 들여다보는 시점이 왔을 때 부담스럽지 않으려면 어떻게 해야 할까? 당연한 말이지만 확장 및 수정이 용이할 코드를 짜야한다.
감사하게도 요 근래 좋은 코드리뷰를 많이 받으면서, 이렇게 하면 조금 더 확장 및 수정이 용이한 코드에 접근할 수 있겠구나! 깨달음을 얻었다. 간략한 코드 예시를 통해서 코드에서 예상할 수 있는 문제점은 무엇인지, 그리고 해당 코드를 좀 더 가독성을 높이고 확장이 용이한 코드로 개선해 볼 수 있는 방법은 무엇일지-`페이지네이션의 버튼들` 편을 공유해보고자 한다.
❪ 목차 ❫
❍ 구현해야 할 것
4개의 페이지가 있다. 각 페이지마다 테이블이 그려져 있고, 테이블 아래에는 동일한 페이지네이션 버튼이 존재한다. 페이지네이션 버튼 UI는 [처음으로] [이전으로] [다음으로] 이렇게 세 개의 버튼이 한 줄로 배열되어 있는 버튼 뭉치이다.
❍ 처음 구현한 것
모든 페이지가 동일한 UI를 갖고 있다 보니 아예 하나의 뭉치로 만들어 재사용하면 좋겠다 싶어 [처음으로] [이전으로] [다음으로]세 개의 버튼을 가진 PaginationButtons component를 만들었다.
function PaginationButtons() {
return (
<flex>
<button>처음으로</button>
<button>이전으로</button>
<button>다음으로</button>
</flex>
)
}
그리고 UI와 로직을 분리하기 위해, 각각의 버튼에는 각 버튼을 클릭하면 외부로 어떤 역할을 하는 버튼인지 actionType을 넘겨주는 기능을 달아주었다. 구현은 아래와 같다.
type PaginationActionType = 'start' | 'prev' | 'next';
interface Props {
onChange: (type: PaginationActionType) => void;
}
function PaginationButtons() {
return (
<flex>
<button onClick={() => {onChange('start')}}>처음으로</button>
<button onClick={() => {onChange('prev')}}>이전으로</button>
<button onClick={() => {onChange('next')}}>다음으로</button>
</flex>
)
}
PaginationButtons의 사용은 아래와 같은 구조이며, PaginationButtons에서 넘겨받은 actionType을 기준으로 nextPage를 getPageNumber으로 넘겨받아 router로 넘겨주었다.
function Pagination() {
const router = useRouter();
const handleChange = (type: PaginationActionType) => {
const nextPage = getPageNumber({ currentPage: page, actionType: type })
router.push({ query: { ...router.query, page: nextPage }});
}
return (
<PaginationButtons onChange={handleChange}/>
)
}
function getPageNumber({
actionType,
currentPage,
}: {
actionType: PaginationActionType;
currentPage: number;
}) {
switch (actionType) {
case 'start':
return DEFAULT_PAGE;
case 'next':
return currentPage + 1;
case 'prev':
return currentPage - 1;
default:
throw new Error('invalid type');
}
}
이렇게 만들어 놓은 코드들로 각 페이지에 페이지네이션을 붙이기 위해서는 아래 코드들을 복붙 하기만 하면 된다.
function Pagination() {
const router = useRouter();
const handleChange = (type: PaginationActionType) => {
const nextPage = getPageNumber({ currentPage: page, actionType: type })
router.push({ query: { ...router.query, page: nextPage }});
}
return (
<PaginationButtons onChange={handleChange}/>
)
}
어떤가? 나는 코드 리뷰를 받기 전까지, 재사용성이 좋은 + 그리고 각 모듈들마다 책임이 분명한 코드를 만들었다고 생각했다. 하지만 해당 코드에는 현재는 드러나진 않았지만, 추후 드러날 수 있는 문제점들이 있다.
❍ 코드에서 예상가능한 문제점
문제 1. 확장 및 변경에 용이하지 않다 - 버튼은 무조건 [처음으로] [이전으로] [다음으로]세 개를 세트로만 쓸 수 있다
만약 "[처음으로] 버튼을 A 페이지에선 빼주시고 B 페이지에서만 넣어주세요." 혹은 "C 페이지에서는 [이전으로]와 [다음으로] 버튼 사이에 [숫자] 버튼들을 넣어주세요." 이런 요청이 들어온다면..? 그렇다면 이 코드는 재사용할 수가 없다. 재사용하기 용이하게 만들었다고 생각했으나, 전혀 재사용에 용이한 코드가 아니게 된 것.
문제 2. Pagination 기능을 완성하기 위해서 함께 해야만 하는 모듈들이 너무 많다
PaginationButtons도 알아야 하고 PaginationActionsType도 알아야 하고 getPageNumber도 알아야 한다. 강결합된 모듈들이 너무 많다. 이럴 경우 동일하게 확장이 어렵고 유지보수가 어려워진다.
❍ 해결방법
문제 1. 확장 및 변경에 용이하지 않다 - 버튼들의 뭉치-PaginationButtons를 Button으로 조각내자!
확장성 및 유지보수를 수월하게 하기 위해서는 사용하는 곳에서 원하는 버튼을 선택하는 책임을 넘기야 한다. 하나의 Buttons 뭉치가 아니라 Button들의 합성으로 버튼들을 조각내주었다.
// as-is
function PaginationButtons() {
return (
<flex>
<button onClick={() => {onChange('start')}}>처음으로</button>
<button onClick={() => {onChange('prev')}}>이전으로</button>
<button onClick={() => {onChange('next')}}>다음으로</button>
</flex>
)
}
// to-be
function Pagination(children: ReactNode) {
return (
<flex>
{children}
</flex>
)
}
interface Props extends HTMLProps<HTMLButtonElement> {}
function Pagination.StartButton = function StartButton(buttonProps: Props) {
return (<button {...buttonProps}>처음으로</button>)
}
function Pagination.PrevButton = function PrevButton(buttonProps: Props) {
return (<button {...buttonProps}>이전으로</button>)
}
function Pagination.NextButton = function NextButton(buttonProps: Props) {
return (<button {...buttonProps}>다음으로</button>)
}
이렇게 조각내놓으면, 이제 위에서 예시로 들었던 요청들을 손쉽게 해결할 수 있다.
- "처음으로 버튼을 A 페이지에선 빼주시고 B 페이지에서만 넣어주세요."
- "C 페이지에서는 이전으로와 다음으로 버튼 사이에 숫자 버튼을 넣어주세요."
// 요청1. `처음으로` 버튼을 A 페이지에선 빼주시고 B 페이지에서만 넣어주세요.
function PagintaionButtons_A () {
return (
<Pagination>
<Pagination.PrevButton />
<Pagination.NextButton />
</Pagination>
)
}
function PagintaionButtons_B () {
return (
<Pagination>
<Pagination.StartButton />
<Pagination.PrevButton />
<Pagination.NextButton />
</Pagination>
)
}
// 요청2. C 페이지에서는 이전으로와 다음으로 버튼 사이에 숫자 버튼을 넣어주세요.
function PagintaionButtons_C () {
return (
<Pagination>
<Pagination.StartButton />
<Pagination.PrevButton />
// ... 무언가 숫자 버튼들
<Pagination.NextButton />
</Pagination>
)
}
문제 2. Pagination 기능을 완성하기 위해서 함께 해야만 하는 모듈들이 너무 많다 - 강결합된 모듈들을 제거하자
이전 코드를 보자면, PaginationButtons를 사용하기 위해서는 PaginationActionType을 알아야 한다. 그리고 pagination 기능은 PaginationActionType으로 PaginationButtons와 연결되어 있다. 뿐만 아니라, pagination 기능은 getPageNumber를 알아야만 사용할 수 있다. getPageNumber는 또 PaginationActionType를 알아야만 값을 반환할 수 있다.
강결합되어 있는 부분들을 정리해 보자면, 아래와 같다
- PaginationButtons와 PaginationActionType의 강결합
- Pagination의 handleChange 기능과 getPageNumber의 강결합
- getPageNumber와 PaginationActionType의 강결합
이렇게 UI-유틸-액션이 아주 쫀쫀하게 결합되어 있으니 요 세 개는 꼭 붙어 다녀야만 하는 존재가 되어버렸다. 꼭 붙어 다녀야만 하는 데에도 불구하고, model, util 각각의 기능마다 모듈 파일도 따로 분리시켜 놓으니 응집도도 너무 낮아졌다.
getPageNumber이나 PaginationActionType가 정말 필요한가?
usePagination hook 내부 모아서 깔끔하게 처리해줘 버리자.
// as-is
function 컴포넌트 () {
// ...
const handleChange = (type: PaginationActionType) => {
const nextPage = getPageNumber({ currentPage: page, actionType: type })
router.push({ query: { ...router.query, page: nextPage }});
// ...
}
// to-be
function usePaginationState() {
const router = useRouter();
const currentPage = router.query['page']
const movePage = (page: number) =>
router.push({
query: { ...router.query, page },
});
return [
currentPage,
{
moveStart: () => movePage(DEFAULT_PAGE),
movePrev: () => movePage(currentPage - 1),
moveNext: () => movePage(currentPage + 1),
}
] as const;
}
hook 하나의 내부 안에 관련된 로직들을 모아, 모듈들의 의존성이 사라짐으로써 강결합 된 부분들도 풀렸다. UI와 로직도 의존성을 분리해 놓아, 원한다면 usePaginationState는 다른 UI와 사용될 수도 있고, Pagination Buttons UI들 또한 다른 함수, 다른 훅들과 사용될 수 있다.
// 사용
function Pagination() {
const [page, { movePrev, moveNext }] = usePaginationState()
return (
<Pagination>
<Pagination.PrevButton onClick={movePrev}/>
<Pagination.NextButton onClikc={moveNext}/>
</Pagination>
)
}
❍ 정리
좋은 코드는 확장 및 수정이 용이한 코드이다.
- UI 재사용을 위해서는 component 합성으로 풀어보자
- 강결합된 모듈들이 발견될 경우, 이것은 정말 모듈화를 시키는 것이 맞는지 의심해 보자
'FE' 카테고리의 다른 글
좋은 코드란 무엇일까?(2) - 하나의 컴포넌트 두개의 쿼리 (0) | 2023.04.23 |
---|