Problem
현재 프로젝트의 Home 컴포넌트에는 불필요한 중복 코드가 많았다. Firestore에서 카테고리별 데이터를 가져와 상태에 저장하고, 이를 Section 컴포넌트에 전달해 UI를 그리는 구조였는데, 문제는 카테고리마다 동일한 로직을 4번이나 반복해야 했다는 점이다.
- techNotes, thoughts, deepdives, portfolio 각각을 별도 상태로 관리
- useEffect 안에서 getPostsByCategory를 4번 호출
- 카테고리가 늘어날수록 코드 수정 범위가 기하급수적으로 증가
처음 블로그 프로젝트를 구현 할 때에는 코드가 문제없이 동작하니 그대로 두었지만, 카테고리 중앙 관리 리팩토링을 진행한 뒤 Home 컴포넌트 역시 구조 개선이 필요하다는 것이 명확해졌다.

Solution
중복을 제거하고 확장성을 높이기 위해 상태와 데이터 구조를 정규화했다. 아래와 같은 방식으로 4개의 중복된 useState 선언과 set 함수 호출을 모두 없앨 수 있었다.
- 상태를 단일 객체로 통합
- Record<Category, Post[]> 형태로 관리해 카테고리별 배열을 하나의 객체로 묶음
- Promise.all + Object.fromEntries 활용
- CATEGORIES.map을 사용해 모든 카테고리를 순회하며 데이터를 병렬로 가져옴
- [category, posts] 튜플을 반환하고 Object.fromEntries로 객체 변환
리팩토링 전 코드 (요약)
export const Home = () => {
const [techNotes, setTechNotes] = useState<Post[]>([]);
const [thoughts, setThoughts] = useState<Post[]>([]);
// ... 동일한 패턴 반복
useEffect(() => {
const fetchData = async () => {
const [tech, thought] = await Promise.all([
getPostsByCategory("tech-notes"),
getPostsByCategory("thoughts"), // ... 동일한 패턴 반복
]);
setTechNotes(tech);
setThoughts(thought); // ... 동일한 패턴 반복
};
fetchData();
}, []);
return (
<main className={styles.main}>
<Gretting />
<Section // ... 동일한 패턴 반복
title={"Tech Notes"}
moreToLink={"/menu/tech-notes"}
description={"개발 관련 기술 노트입니다"}
posts={techNotes}
/>
</main>
);
};
리팩토링 후 코드
- Promise.all + CATEGORIES.map으로 동일 로직의 반복을 제거
- results의 반환값을 [category, posts] 튜플로 반환하여 category와 post를 짝지어 반환할 수 있게 함.
( post만 반환하면 어떤 category인지 알 수 없기 때문 ) - fireStore에서 받아온 [category , post [] ] 튜플 데이터터를 Object.fromEntries()로 객체로 변환하여 사용함
⇒ [ ["tech-notes", [ Post, Post ] ], ["thoughts", [ Post, Post ] ], ] ⇒ { "tech-notes": [ ... ], "thoughts", [ … ], }
type PostsByCategory = Record<Category, Post[]>;
export const Home = () => {
const [postsByCategory, setPostsByCategory] = useState<PostsByCategory>({
"tech-notes": [],
thoughts: [],
deepdives: [],
portfolio: []
});
useEffect(() => {
const fetchAll = async () => {
const results = await Promise.all(
CATEGORIES.map(async (category) => {
const posts = await getPostsByCategory(category);
return [category, posts] as const;
})
);
const mapped = Object.fromEntries(results) as PostsByCategory;
setPostsByCategory(mapped);
};
fetchAll();
}, []);
return (
<main className={styles.main}>
{CATEGORIES.map((c, idx) => {
return (
<Section
key={idx}
title={CATEGORY_META[c].label}
moreToLink={paths.menu({ category: c })}
description={CATEGORY_META[c].description}
posts={postsByCategory[c]}
/>
);
})}
</main>
);
};
Result
리팩토링 후 얻은 효과는 다음과 같다!
- 개별 상태를 없애고 단일 객체로 통합하여 상태 관리를 단순화하였다. ( 코드량은 줄이고 확장성은 높임 )
- 4번이나 반복되던 로직을 map 순회로 변경하여 중복 코드를 제거하였다. ( UI렌더링 / 카테고리 별 데이터 로드 시 )
- Category와 Router의 중앙 관리를 적용하여 확장성을 향상시켰고, paths.menu를 사용하여 오타와 불일치를 방지하였다.
Develop Key
- Object.fromEntries()는 키-값 쌍 배열을 객체로 변환하는 메서드.
const obj = Object.fromEntries([ ["a", 1], ["b", 2]]);
// obj = { a: 1, b: 2 }
- DB의 반환값을 튜플로 반환하여 key와 value를 한번에 저장하기 [category, posts]라는 튜플을 만들면, 첫 번째 값: key (Category 문자열) // 두 번째 값: value (해당 글 목록 Post[]) 으로 반환할 수 있음 → 이렇게 반환된 데이터는 Object.fromEntries() 를 사용해 객체 변환할 수 있다.
[ ["tech-notes", [Post, Post]], ["thoughts", [Post, Post]] ]
=> 해당 카테고리와 값을 객체로 변환해 사용할 수 있게 함
- Promise.all의 정의 및 사용법 여러 개의 Promise를 동시에 실행하고, 모두 성공했을 때 결과를 배열로 반환하는 함수.
이로써 눈에 보이던 가장 큰 리팩토링을 한 것 같다.
데이터를 중앙 관리화 시켜 놓으니 그와 관련된 부분에서도 리팩토링 하기가 훨씬 수월해졌다.
이는 나 혼자 개발하는 것 뿐만 아니라 팀원과 개발할 때도 유용하게 적용될 팁인 것 같다.
'개발공부_Blog > Project' 카테고리의 다른 글
| React Router + Zod: 안전한 params 검증과 useLoaderData 활용법 (1) | 2025.09.22 |
|---|---|
| 하드코딩 된 카테고리, 중앙 관리로 리팩토링[2] (0) | 2025.09.18 |
| 하드코딩 된 라우트 경로, 중앙 관리로 리팩토링[1] (0) | 2025.09.18 |
| Vercel로 [ Vite + React + Firebase ] 프로젝트 배포하기 (4) | 2025.08.17 |
| Firebase Authentication으로 로그인 구현하기 (6) | 2025.08.17 |
댓글