본문 바로가기
개발공부_Blog/Project

Home 컴포넌트 ⇒ 중복 데이터 로딩, 단일 상태 관리로 해결하기

by 소팡팡 2025. 9. 18.

Problem

현재 프로젝트의 Home 컴포넌트에는 불필요한 중복 코드가 많았다. Firestore에서 카테고리별 데이터를 가져와 상태에 저장하고, 이를 Section 컴포넌트에 전달해 UI를 그리는 구조였는데, 문제는 카테고리마다 동일한 로직을 4번이나 반복해야 했다는 점이다.

  • techNotes, thoughts, deepdives, portfolio 각각을 별도 상태로 관리
  • useEffect 안에서 getPostsByCategory를 4번 호출
  • 카테고리가 늘어날수록 코드 수정 범위가 기하급수적으로 증가

처음 블로그 프로젝트를 구현 할 때에는 코드가 문제없이 동작하니 그대로 두었지만, 카테고리 중앙 관리 리팩토링을 진행한 뒤 Home 컴포넌트 역시 구조 개선이 필요하다는 것이 명확해졌다.

 

 

Solution

중복을 제거하고 확장성을 높이기 위해 상태와 데이터 구조를 정규화했다. 아래와 같은 방식으로 4개의 중복된 useState 선언과 set 함수 호출을 모두 없앨 수 있었다.

  1. 상태를 단일 객체로 통합
    • Record<Category, Post[]> 형태로 관리해 카테고리별 배열을 하나의 객체로 묶음
  2. 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를 동시에 실행하고, 모두 성공했을 때 결과를 배열로 반환하는 함수.

 

 

이로써 눈에 보이던 가장 큰 리팩토링을 한 것 같다.

데이터를 중앙 관리화 시켜 놓으니 그와 관련된 부분에서도 리팩토링 하기가 훨씬 수월해졌다.

이는 나 혼자 개발하는 것 뿐만 아니라 팀원과 개발할 때도 유용하게 적용될 팁인 것 같다.

댓글