마이페이지 현황과 리팩토링 이유
현재 마이페이지의 View는 아래와 같습니다.
웹뷰

모바일 뷰

현재 프로젝트가 강화와 시바 관련 2가지 내용 밖에 없기에 2가지 그룹으로 이루어져 있습니다.
Apple의 Compositional Layout을 참고해서 마이페이지를 그룹화 하면 아래와 같습니다.
section : 노랑, group :파랑 , item : 초록

하지만 현재 제 코드는 너무 투박합니다..
import { auth } from '@/auth';
import EnforceRecord from '@/features/mypage/EnforceRecord';
import ShibaRecord from '@/features/mypage/ShibaRecord';
import styles from '@/app/mypage/mypage.module.css';
export default async function Mypage() {
const session = await auth();
return (
<div className={styles.recordWrapper}>
<div className={styles.container}>
<EnforceRecord userEmail={session?.user?.email!} />
<ShibaRecord userEmail={session?.user?.email!} />
</div>
</div>
);
}
그저 EnfoceRecord랑 ShibaRecord가 있구나.. className을 통해서 css를 일부분 공유하고 나름 구조도 통일성 있게 할려했지만 관심사 분리도 되어있지 않고 그저 투박합니다..
import { getAndCleanupEnforceRecords } from '@/remote/enforcement';
import { convertTimestampToKoreanDate } from '@/shared/utils/date';
import styles from '@/app/mypage/mypage.module.css';
export default async function EnforceRecord({
userEmail,
}: {
userEmail: string;
}) {
const records = await getAndCleanupEnforceRecords(userEmail);
return (
<div className={styles.recordContainer}>
<h2 className={styles.title}>
<div>강화 기록</div>
<div className={styles.statusIndicator}>
<span className={styles.successBox}></span>성공
<span className={styles.failureBox}></span>실패
</div>
</h2>
<ul className={styles.recordList}>
{records.map(({ id, percent, status, date }) => (
<li
key={id}
className={`${styles.recordItem} ${
status === '성공' ? styles.success : styles.failure
}`}
>
<span>{convertTimestampToKoreanDate(date)}</span>
<span>
{percent}%<span className={styles.deskVisible}>{status}</span>
</span>
</li>
))}
</ul>
</div>
);
}
현재 이 상태로 새로운 페이지가 생긴다면,, 그저 복붙을 하면서 다시 꾸미겠죠.. 확장성이 굉장히 없어보입니다..
따라서 최근 읽은 카카오 아티클(프론트엔드와 SOLID 원칙)을 참고하여 리팩토링을 진행하도록 결심하였습니다.
리팩토링 진행 과정
우선 공유되는 내용과 아닌 내용을 분리하고 어떻게 리팩토링 할지에 대해서 고민을 시작하였습니다.

공통점
1. grid layout, 반응형 breakpoint 등...
2. header, content 의 group 구조.
3. 좌측 내용, 우측 부가 정보 있는 group구조
차이점
1. 왼쪽 제목은 택스트만 바뀌면 되지만 오른쪽에 보면 강화기록의 경우는 성공 실패의 색을 알려주고,
시바획득목록은 현재 수집 개수 /총 개수를 보여주고 있습니다.
2. 아이템의 크기나 space between css를 먹인 것은 비슷하지만, 강화 기록은 색이 있고, 시바 획득 목록에는 색이 없죠..
3. 불러오는 api 데이터
추후 컴퍼넌트가 추가되도 section 자체는 비슷할 것입니다. 따라서 우선 공통적으로 사용할 RecordSection을 만들어줍니다.
import React from 'react';
import styles from '@/app/mypage/mypage.module.css';
interface RecordSectionProps<T> {
title: string;
subInfo: React.ReactNode;
records: T[];
content: (record: T) => React.ReactNode;
}
export default function RecordSection<T>({
title,
subInfo,
records,
content,
}: RecordSectionProps<T>) {
return (
<div className={styles.recordContainer}>
<div className={styles.title}>
<h2>{title}</h2>
<div className={styles.subInfo}>{subInfo}</div>
</div>
<ul className={styles.recordList}>
{records.map((record) => content(record))}
</ul>
</div>
);
}
1. content부분에는 어떤 데이터가 들어올지 명확하지 않죠.. 따라서 제네릭 타입이 적합합니다..
2. 그리고 title,이나 records 자체가 들어오는 사실은 명확하나 subInfo나 content의 UI자체는 조금씪 달랐습니다. 따라서
ReactNode를 매개변수로 전달받아 좀 더 유연하게 만들었습니다.
이제 page.tsx 파일로 이동합니다. 기존에는 각각의 Component를 직접 넣어서 값과 액션이 구분 되지 않았습니다..
이를 분리시키고 map을 통해서 랜더링 시켜줍니다.
import { auth } from '@/auth';
import EnforceRecord from '@/features/mypage/EnforceRecord';
import ShibaRecord from '@/features/mypage/ShibaRecord';
import styles from '@/app/mypage/mypage.module.css';
const records = [
{ type: 'ENFORCE', component: EnforceRecord },
{ type: 'SHIBA', component: ShibaRecord },
];
export default async function Mypage() {
const session = await auth();
const userEmail = session?.user?.email!;
return (
<div className={styles.recordWrapper}>
<div className={styles.container}>
{records.map(({ type, component: Record }) => (
<Record key={type} userEmail={userEmail} />
))}
</div>
</div>
);
}
마지막으로 각각의 컴퍼넌트를 리팩토링합니다.
import { getAndCleanupEnforceRecords } from '@/remote/enforcement';
import { convertTimestampToKoreanDate } from '@/shared/utils/date';
import styles from '@/app/mypage/mypage.module.css';
import { RecordProps } from './constant/model';
import { Timestamp } from 'firebase/firestore';
import RecordSection from './RecordSection';
interface EnforceContent {
id: string;
percent: number;
status: string;
date: Timestamp;
}
export default async function EnforceRecord({ userEmail }: RecordProps) {
const records = await getAndCleanupEnforceRecords(userEmail);
const renderItem = ({ id, percent, status, date }: EnforceContent) => (
<li
key={id}
className={`${styles.recordItem} ${
status === '성공' ? styles.success : styles.failure
}`}
>
<span>{convertTimestampToKoreanDate(date)}</span>
<span>
{percent}%<span className={styles.deskVisible}>{status}</span>
</span>
</li>
);
return (
<RecordSection
title="강화 기록"
subInfo={
<>
<span className={styles.successBox}></span>성공
<span className={styles.failureBox}></span>실패
</>
}
records={records}
content={renderItem}
/>
);
}
각각의 group과 item별로 값을 RecordSection에 props로 전달합니다.
아까 말했떤 대로 subInfo 와 content의 경우에는 직접 jsx를 작성해서 전달합니다.
느낀점
막상 끝나고 보면 별 내용이 아닌거 같네요... 그래도 개선되었고 조금은 더 SOLID 해지지 않았나 생각이 듭니다.
설계에 집중하다 보니, 진행사항이 더딘거 같아서 우선 완성하고 다시 리팩토링해보자는 마음으로 만든게 조금 티가 나네요...
그리고 금방 리팩토링 할 줄 알았는데, 시간이 꽤 걸렸네요... 그래도 이런 작업을 하다 보면 '어느 순간에는 좋은 설계를 자연스럽게 할 수 있지 않을까.?'라는 기대감을 안고 마칩니다..
참고 문헌
https://fe-developers.kakaoent.com/2023/230330-frontend-solid/
프론트엔드와 SOLID 원칙 | 카카오엔터테인먼트 FE 기술블로그
임성묵(steve) 판타지, 무협을 좋아하는 개발자입니다. 덕업일치를 위해 카카오페이지로의 이직을 결심했는데 인사팀의 실수로 백엔드에서 FE개발자로 전향하게 되었습니다. 인생소설로는 데로
fe-developers.kakaoent.com
'프로젝트' 카테고리의 다른 글
시바 컴퍼넌트 관심사 분리하여 결합도 낮추기 (0) | 2024.07.22 |
---|---|
goodluck 프로젝트 폴더 구조와 좋은 구조에 대한 생각과 피드백 (0) | 2024.07.05 |
Draco를 활용하여 glb파일 압축하여 성능 향상시키기 (0) | 2024.07.01 |
ManulPopup창을 관심사 분리하기 (값,계산,액션, SOLID) (0) | 2024.06.29 |
R3F 및 useCannon에서 시바 조종하기 (최종 이동 로직 구현 및 채택 과정) (0) | 2024.06.28 |