현재 트래모리라는 여행 기획, 계획을 기록하고 공유하는 웹 애플리케이션 프로젝트를 진행 중이다. 초기 퍼블리싱 기간 동안 작업한 코드는 나름 재사용이 가능한 컴포넌트 구조로 작업을 했는데 뷰와 로직 분리가 전혀 되어있지 않고 공용 컴포넌트의 스타일 정의, 훅 함수 분리가 필요한 부분 등에 대한 리팩터링 기간이 필요했다.
...
(간단한 css 레이아웃에 대한 수정은 생략하고 주요 리팩터링 부분만 아카이빙 하기로 함)
: 개별 토글하는 부분이 현재 재사용되고 있고 추후에 쓰일 가능성이 있어 hooks 폴더에 생성해주었다.
import { useAtom } from 'jotai';
export const useIndiviualToggle = (atomCreator, id) => {
const [toggleState, setToggleState] = useAtom(atomCreator(id));
const handleToggle = () => {
setToggleState((prev: boolean) => !prev);
};
return {
toggleState,
setToggleState,
handleToggle,
};
};
export const OneBadgeSlide = ({ item, id }: OneBadgeSlideProps) => {
const { title, description, info } = item;
const [isIndividualToggle, setIsIndividualToggle] = useAtom(
isIndividualOneBadgeToggleAtom(id)
);
// 생략
}
export const OneBadgeSlide = ({ item, id }: OneBadgeSlideProps) => {
const { title, description, info } = item;
const {
toggleState: isIndividualToggle,
setToggleState: setIsIndividualToggle,
} = useIndiviualToggle(isIndividualOneBadgeToggleAtom, id);
// 생략
}
export const OneInquiryHistory = ({ data }: InquiryDataProps) => {
const { toggleState: isIndividualToggle, handleToggle } = useIndiviualToggle(
isIndividualToggleAtom,
data.id
);
// 생략
}
export const FlagInfo = ({ data, id }: FlagInfoDataProps) => {
const { toggleState: isIndividualToggle, handleToggle } = useIndiviualToggle(
isIndividualFlagToggleAtom,
id
);
➡️ 중복되는 로직을 훅함수로 적용할 수 있고 view와 logic 분리를 할 수 있다.
: 페이지네이션의 현재 페이지, 페이지 이동 감지, 페이지 당보여줄 개수, 첫번째, 마지막 인덱스 등 페이지네이션에 쓰여야 하는 정보와 상태가 많다. 이를 페이지네이션을 사용하는 페이지마다 작성하지 않고 간단한 커스텀을 통해 사용할 수 있도록 훅함수를 만들었다.
import { useState } from 'react';
export const usePagination = (itemsPerPage: number) => {
const [currentPage, setCurrentPage] = useState(0);
const startIdx = currentPage * itemsPerPage;
const endIdx = (currentPage + 1) * itemsPerPage;
return { currentPage, setCurrentPage, startIdx, endIdx, itemsPerPage };
};
export const ViewStory = () => {
const [currentPage, setCurrentPage] = useState(0);
const [itemsPerPage] = useState(4);
const startIdx = currentPage * itemsPerPage;
const endIdx = (currentPage + 1) * itemsPerPage;
// 생략
export const ViewStory = () => {
const { currentPage, setCurrentPage, startIdx, endIdx, itemsPerPage } =
usePagination(4);
// 생략
➡️ 이 또한 페이지네이션을 사용하는 컴포넌트의 코드 중복을 줄이고 기능을 분리한다는 장점이 있다.
Select.style.ts
export const getSelectClasses = (variant: string, isToggle: boolean) =>
({
service:
'w-[700px] flex justify-between border py-2 px-3 rounded text-sm leading-6 cursor-pointer',
mypageCategory:
'w-[160px] pl-4 pr-2 py-1 flex justify-end border border-primaryBlue-700 text-sm leading-6 cursor-pointer',
modalCategory: `w-[300px] flex justify-between border py-[10px] px-4 rounded text-sm leading-6 cursor-pointer ${
isToggle ? 'border-[#6EA5FF]' : ''
}`,
}[variant]);
export const getOptionClasses = (variant: string) =>
({
service:
'w-[700px] flex flex-col border pb-2 px-3 rounded text-sm leading-6 cursor-pointer bg-white/90 top-11',
mypageCategory:
'w-[160px] pb-2 px-3 flex flex-col items-center border border-primaryBlue-700 text-sm leading-6 cursor-pointer top-10',
modalCategory:
'w-[300px] flex flex-col border pb-2 px-3 rounded text-sm leading-6 cursor-pointer bg-[#EEF5FF]/90 top-11 border-[#6EA5FF] top-[52px]',
}[variant]);
export const getMypageCategoryClasses = (variant: string) =>
({
mypageCategory: 'ml-[30px]',
}[variant]);
export const getOneOptionClasses = (variant: string) =>
({
service: 'hover:font-bold mt-2',
mypageCategory: 'hover:font-bold mt-2',
modalCategory:
'hover:font-bold hover:text-primaryBlue-300 mt-2 text-primaryGray-500',
}[variant]);
import { cva } from 'class-variance-authority';
export const selectVariants = cva(
'relative cursor-pointer', // 공통적으로 적용될 스타일
{
variants: {
variant: {
service:
'w-[700px] flex justify-between border py-2 px-3 rounded text-sm leading-6',
modalCategory:
'w-[300px] flex justify-between border py-[10px] px-4 rounded text-sm leading-6',
},
},
defaultVariants: {
variant: 'service',
},
}
);
export const optionVariants = cva(
'flex flex-col border pb-2 px-3 rounded text-sm leading-6 cursor-pointer top-11',
{
variants: {
variant: {
service: 'w-[700px] bg-white/90',
modalCategory: 'w-[300px] bg-[#EEF5FF]/90 border-[#6EA5FF] top-[52px]',
},
},
defaultVariants: {
variant: 'service',
},
}
);
export const oneOptionVariants = cva('mt-2', {
variants: {
variant: {
service: 'hover:font-bold',
modalCategory:
'hover:font-bold hover:text-primaryBlue-300 text-primaryGray-500',
},
},
defaultVariants: {
variant: 'service',
},
});
CSS 클래스를 관리하기 위한 코드가 더욱 명확하고 직관적으로 보입니다. 각 variant에 필요한 스타일을 한눈에 파악할 수 있다.
cva 첫 번째 인자에 공통적으로 사용되는 스타일을 모아서 관리할 수 있다. 이는 중복 코드를 줄여 코드 양을 줄이고 유지보수에도 좋다
디폴트 variant를 설정하여 기본적으로 적용되는 스타일을 정의할 수 있다. 이를 통해 별도의 조건문 없이도 기본 스타일을 적용할 수 있다.
모든 style을 한꺼번에 넣어 좀 더 사용하기 편하도록 만드려고 했는데 스타일 적용이 잘 되지 않아 옵션별로 개별 스타일 적용을 해주었다. 리팩터링으로 인해 코드가 많이 개선된 것 같지는 않아 조금 아쉽다..! 더 좋은 방법이 있을 것이니 개발하면서 찾아볼 생각이다.
예를 들어 마이페이지의 나의 배지 발급 페이지가 있다고 했을 때, 해당 컴포넌트에 Tab이 존재했고,
all
,여행 계획
,여행 기록
,이벤트
,방문 대륙
별로 페이지 동적 라우팅을 진행했다. 하지만 이는 Next로 개발했을 때 SSR 특징인 페이지가 변경되었을 때 서버에서 데이터를 전부 받아와 다운받은 후에 렌더링한 결과를 클라이언트 측에 제공하므로 페이지 이동 시 CSR 방식보다 느리게 렌더링 되는 경우가 발생했다.
Tab을 클릭할 경우 페이지 라우팅이 아닌
useSearchParams
를 사용하여 Client Component hook 방식으로 변경했다. 라우팅이 아닌 컴포넌트를 갈아끼우는 방식을 사용한 것이다.
ex) mypage/badge/record
-> mypage/badge?filter=record
초기 동적 라우팅을 진행할 때는
폴더 구조를 mypage/badge/[slug]
로 만들어주어 동적 라우팅을 진행했다.
// mypage/badge/[slug]/page.tsx
import { usePathname } from 'next/navigation';
const MyPageTabs = () => {
const pathname = usePathname();
const slug = pathname.split('/').pop();
const basePath = pathname.replace(/\/[^/]+$/, '');
const navTitle = mypageNavConfig.nav.find(
(nav) => nav.href === basePath
).title;
badge config에서 nav.href와 basePath가 같은 것을 찾아 mypage/badge/record
, mypage/badge/plan
이런식으로 [slug]
를 동적으로 라우팅해 해당 데이터만 보여주게 하여 라우팅을 하였는데
리팩터링 시 slug 파일을 아예 제거하고 MyBadgeComponent
컴포넌트를 생성해 params로 연결해주었다
// MyBadgeComponent/MyBadgeComponent.tsx
import { usePathname, useSearchParams } from 'next/navigation';
export const MyBadgeComponent = () => {
const pathname = usePathname();
const params = useSearchParams();
const category = params.get('filter');
const navTitle = mypageNavConfig.nav.find(
(nav) => nav.href === pathname
).title;
위와 같이 해주면 링크가 각 mypage/badge?filter=record
, mypage/badge?filter=filter
와 같은 방식으로 변경되고 Tab의 기능에 맞게 해당 탭 클릭시 params가 추가되고 해당하는 데이터만을 보여주는 것이다.
페이지가 아닌 컴포넌트를 갈아끼우는 부분이라고 생각이 되어 해당 방식으로 리팩터링 하였고 이는 개발 시에도 빠르게 페이지가 변경된 것을 볼 수 있고, 배포 시에도 적용하여 사용자에게 더 빠른 UI를 제공할 수 있다는 점이다.