고민 1) 컴포넌트를 분리할까?
컴포넌트 분리를 고민하는 컴포넌트는 위 사진과 같은 PaginationNav 컴포넌트이다.
코드는 다음과 같다.
컴포넌트를 분리하기 전
export default function PaginationNav({ courseCount = 0 }: PaginationNavProps) {
return (
<nav className="flex justify-center">
<ArrowLeft />
{new Array(lastPage - firstPage + 1)
.fill(0)
.map((_, index) => index + firstPage)
.map((page) => (
<button>
{page}
</button>
))}
<ArrowRight />
</nav>
)
}
전체 코드
export default function PaginationNav({ courseCount = 0 }: PaginationNavProps) {
const searchParams = useSearchParams()
const currentPage = useMemo(
() => Number(searchParams.get('page')) || 1,
[searchParams],
)
const searchParamsRouter = useSearchParamsRouter()
const handlePageClick = useCallback(
(page: number) => {
searchParamsRouter.set('page', String(page))
},
[searchParamsRouter],
)
const maxPage = useMemo(() => Math.ceil(+courseCount / 20), [courseCount])
const isFirst = useMemo(() => currentPage === 1, [currentPage])
const isLast = useMemo(() => currentPage === maxPage, [currentPage, maxPage])
const firstPage = useMemo(
() => (currentPage - 4 > 0 ? currentPage - 4 : 1),
[currentPage],
)
const lastPage = useMemo(
() => (currentPage + 4 < maxPage ? currentPage + 4 : maxPage),
[currentPage, maxPage],
)
const handleLeftClick = useCallback(() => {
const page = Number(searchParamsRouter.get('page') || 1)
if (page === 1) return
searchParamsRouter.set('page', String(page - 1))
}, [searchParamsRouter])
const handleRightClick = useCallback(() => {
const page = Number(searchParamsRouter.get('page') || 1)
if (page === maxPage) return
searchParamsRouter.set('page', String(page + 1))
}, [searchParamsRouter, maxPage])
return (
<nav className="flex justify-center">
<ArrowLeft
fill={isFirst ? '#CCC' : '#222'}
className="cursor-pointer"
onClick={handleLeftClick}
/>
{new Array(lastPage - firstPage + 1)
.fill(0)
.map((_, index) => index + firstPage)
.map((page) => (
<button
key={page}
className={cn('h-24 w-24 rounded-4', {
'text-gray-6 hover:font-bold hover:text-purple-1':
page !== +currentPage,
'bg-purple-1 text-white-1': page === +currentPage,
})}
onClick={() => handlePageClick(page)}
>
{page}
</button>
))}
<ArrowRight
fill={isLast ? '#CCC' : '#222'}
className="cursor-pointer"
onClick={handleRightClick}
/>
</nav>
)
}
{new Array(lastPage - firstPage + 1)
.fill(0)
.map((_, index) => index + firstPage)
.map((page) => (
<button>
{page}
</button>
))}
이 배열에 대한 코드가 컴포넌트 흐름과 다르다는 느낌이 있어 분리를 하면 가독성이 더 좋지 않을까? 고민을 했다.
컴포넌트를 분리했을 때
export default function PaginationNav({ courseCount = 0 }: PaginationNavProps) {
return (
<nav className="flex justify-center">
<ArrowLeft />
<PaginationList currentPage={currentPage} maxPage={maxPage} />
<ArrowRight />
</nav>
)
}
function PaginationList({ currentPage, maxPage }: PaginationListProps) {
return (
<div>
{new Array(shownLastPage - shownFirstPage + 1)
.fill(0)
.map((_, index) => index + shownFirstPage)
.map((page) => (
<button>
{page}
</button>
))}
</div>
)
}
위와 같은 코드가 될 것이다.
전체 코드
'use client'
import useSearchParamsRouter from '@/hooks/useSearchParamsRouter'
import { cn } from '@/utils'
import { useCallback, useMemo } from 'react'
import ArrowLeft from '~/svgs/arrow_left.svg'
import ArrowRight from '~/svgs/arrow_right.svg'
interface PaginationNavProps {
courseCount: number
}
export default function PaginationNav({ courseCount = 0 }: PaginationNavProps) {
const searchParamsRouter = useSearchParamsRouter()
const currentPage = useMemo(
() => Number(searchParamsRouter.get('page')) || 1,
[searchParamsRouter],
)
const maxPage = useMemo(() => Math.ceil(+courseCount / 20), [courseCount])
const isFirst = useMemo(() => currentPage === 1, [currentPage])
const isLast = useMemo(() => currentPage === maxPage, [currentPage, maxPage])
const handleLeftClick = useCallback(() => {
if (currentPage === 1) return
searchParamsRouter.set('page', String(currentPage - 1))
}, [searchParamsRouter, currentPage])
const handleRightClick = useCallback(() => {
if (currentPage === maxPage) return
searchParamsRouter.set('page', String(currentPage + 1))
}, [searchParamsRouter, maxPage, currentPage])
return (
<nav className="flex justify-center">
<ArrowLeft
fill={isFirst ? '#CCC' : '#222'}
className="cursor-pointer"
onClick={handleLeftClick}
/>
<PaginationList currentPage={currentPage} maxPage={maxPage} />
<ArrowRight
fill={isLast ? '#CCC' : '#222'}
className="cursor-pointer"
onClick={handleRightClick}
/>
</nav>
)
}
interface PaginationListProps {
currentPage: number
maxPage: number
}
function PaginationList({ currentPage, maxPage }: PaginationListProps) {
const searchParamsRouter = useSearchParamsRouter()
const handlePageClick = useCallback(
(page: number) => {
searchParamsRouter.set('page', String(page))
},
[searchParamsRouter],
)
const shownFirstPage = useMemo(
() => (currentPage - 4 > 0 ? currentPage - 4 : 1),
[currentPage],
)
const shownLastPage = useMemo(
() => (currentPage + 4 < maxPage ? currentPage + 4 : maxPage),
[currentPage, maxPage],
)
return (
<div>
{new Array(shownLastPage - shownFirstPage + 1)
.fill(0)
.map((_, index) => index + shownFirstPage)
.map((page) => (
<button
key={page}
className={cn('h-24 w-24 rounded-4', {
'text-gray-6 hover:font-bold hover:text-purple-1':
page !== +currentPage,
'bg-purple-1 text-white-1': page === +currentPage,
})}
onClick={() => handlePageClick(page)}
>
{page}
</button>
))}
</div>
)
}
컴포넌트를 역할에 맞게 분리하는 게 항상 맞을까?
곰곰히 생각해보면 PaginationList 또한 PaginationNav역할을 하는게 맞다. 그런데 코드가 조금 더 길다고, 추상화 수준을 맞춘다고 컴포넌트를 또 분리하는 게 맞을까?에 대한 고민이다.
분리하기 전과 분리한 후를 비교
-
가독성이 더 좋은가?
: 가독성은 더 깨진다고 생각한다. 컴포넌트를 읽다가, 갑자기 새로운 컴포넌트가 등장하여 아래로 스크롤을 내려서 읽고 올라와야 한다.
-
유지보수성이 더 좋은가?
: 유지보수성은 좋을 수도 있고, 좋지 않을 수도 있다고 생각한다. PaginationNav에서 사용하는 요소를 가져다가 사용하려면 props로 또 전달해줘야 하기에 더 번거로워진다. 하지만, PaginationList에서만 변경사항이 생긴다면 수정이 다소 유용할 수 있다.
-
재사용성이 더 좋은가?
: PaginationList만 가져다가 사용할 일은 없을 거 같다.
-
책임에 맞게 잘 분리하였는가?
: 위에서도 말했듯이 PaginationList PaginationNav의 역할이며, ArrowLeft와 ArrowRight의 역할과 크게 벗어나지 않는다. 책임이 같은데도, 불필요하게 또 쪼깬듯한 느낌이 든다.
결론
추상화 수준이 다르다는 이유로 컴포넌트를 분리하면 가독성이 더 좋아지지 않을까? 하는 생각에서 코드를 직접 작성해보고 유지보수성, 재사용성, 가독성, 책임의 다양한 방면에서 고민해보았다.
결론은, 컴포넌트를 분리하는 게 항상 좋은 것만은 아니라는 것이다. 그리고 추상화 수준을 맞추는게 항상 좋은 게 아니라는 것을 깨달았다.
고민 2) api 함수를 분리할까?
함수에 대해 설명하자면 우선, searchParamsMap객체를 전달받는다. 그리고 이 Map객체의 값을 바탕으로 constants상수에서 필요한 정보를 얻어와 query parameter로 만든다. 이 query parameter를 api의 url에 담아 서버에 데이터를 요청한다. 이 때, Map객체를 바탕으로 query parameter으로 만들 때의 로직이 정말 난잡하다. 유지 보수성이 너무나 좋지 않다는 것을 느꼈다. 추가사항이나, 수정사항이 생겼을 때? 상상만 해도 진절머리가 난다.
getCourse라는 하나의 역할을 수행하는 상수, 함수, 타입이기에 하나의 파일(api/course.ts)에 몰아넣었다. 이것을 각 유형에 맞게 분리하는 게 맞을까? 상수는 apis/course/constants, 함수는 apis/course/utils, 타입은 apis/course/types 이런 식으로 말이다. 그리고 getCourse 메인함수는 apis/course/api에 넣지 않을까 싶다.
이렇게 분리했을 때, 어떨까?
분리하기 전과 분리한 후를 비교
-
가독성이 더 좋은가?
: 읽기 정말 힘들다. api 함수를 읽다가 이 함수는 뭐지? → utils파일을 찾는다. → 이 타입은 뭐지? types파일을 찾는다 .. 코드를 읽기 위해서는 파일을 넘나들어야한다.
-
유지보수성이 더 좋은가?
: 가독성과 느낌이 비슷하다. 하나의 타입을 추가한다 → 해당 타입을 사용하는 함수의 파일을 찾아가 추가한다 .. 과정이 더 생기는 느낌이다.
-
재사용성이 더 좋은가?
: course에 너무나 종속되는 함수, 타입, 상수이기에 재사용할 일이 없고, 하기 힘들다.
-
책임에 맞게 잘 분리하였는가?
: 책임은 너무나 잘 분리되어있다.
결론
책임에 맞게 각 파일로 분리했을 때, 여러가지 측면에서 깊이 있게 고민을 해보았다.
두 가지 경우 모두 “분리하지 말자”였다. 각 역할에 맞게 파일을 분리 했을 때, 책임에 맞게 잘 분리가 되었을 지는 모른다. 하지만, 분리를 한다고 무조건 좋은 게 아니라는 것을 깨달았다. 가독성, 유지보수성, 재사용성 또한 고려를 해야 한다.
이러한 함수, 컴포넌트에 대한 나만의 분리 원칙을 만들어야겠다고 강하게 느꼈다. 이러한 규칙이 수립되어 있지 않다면 상황에 따라, 컨디션에 따라 갈대처럼 흔들릴 것이다. 만약 팀에 속해있다면, 팀원들과 논의를 하여 이러한 규칙을 수립해야겠다.