3 분 소요

안녕하세요 오랜만입니다! 오늘은 리액트 프로젝트에 HOC(Higher-Order Component) 패턴을 도입하게 된 과정을 공유해보려고 합니다!


기존에는 3개의 게시판이 유사한 UI와 로직을 공유하고 있었기 때문에 :boardName 경로를 여러 번 선언하는 방식으로 구현했었는데요. 이 방식에 문제가 있다는 걸 알게 되었습니다.

🔥 문제 상황

처음엔 다음과 같이 게시판마다 다른 라우트를 선언했습니다.

<Route path="/:boardName" element={<DakkuGallery />} />
<Route path="/:boardName" element={<ReviewAndTips />} />
<Route path="/:boardName" element={<DakkuQnA />} />

React Router v6에서는 동일한 경로(/boardName)가 여러 번 선언되면 가장 먼저 매칭되는 한 개의 라우트만 렌더링됩니다. 따라서 위의 코드처럼 /:boardName 경로를 세 번 선언했을 경우에는 가장 위에 있는 DakkuGallery만 렌더링되고 ReviewAndTips, DakkuQnA는 무시됩니다.


처음엔 이 구조에 문제가 있다는 걸 눈치채지 못했습니다. 왜냐하면 겉보기엔 게시판이 모두 잘 작동하고 있었거든요.


라우팅 문제 이미지

하지만 React Dev Tools의 Components 탭에서 확인해보니 항상 DakkuGallery만 렌더링되고 있다는 사실을 발견할 수 있었습니다. 그렇다면 이런 문제가 있음에도 어째서 잘 작동했던 걸까요?

🤔 왜 문제처럼 보이지 않았을까?

const { boardName } = useParams();

useEffect(() => {
  const fetchPosts = async () => {
    const fetchedPosts = await getPost(boardName);
    setPosts(fetchedPosts);
  };
  fetchPosts();
}, [boardName]);

그 이유는 Board 컴포넌트 내부에서 useParams()를 사용해 URL의 boardName 값을 추출하고, 그에 따라 게시판 데이터를 fetch하는 방식으로 동작하고 있었기 때문입니다.
즉, 실제로는 DakkuGallery 하나만 렌더링되고 있었지만, useParamsuseEffect 내부의 동적 처리 덕분에 다른 게시판도 잘 작동하는 것처럼 보였던 것이었죠.


따라서 저는 이 문제를 해결하고자 했고, 그 과정에서 Custom Hook 과 HOC(Higher-Order Component) 구조를 고민하게 되었습니다!

💡 커스텀 훅 vs. HOC

기준 커스텀 훅 HOC
공통 구조 활용 게시판 구조가 거의 동일할 때 유리 게시판별로 기능이 다르게 발전할 경우 유리
유지보수성 설정만 추가하면 확장 가능 구조적으로 역할 분리 가능 → 단일 책임 원칙 충족
확장 가능성 설정만 추가하면 쉽게 게시판 추가 게시판마다 UI/로직이 달라질 경우 유연하게 대응 가능
SEO & 코드 분할 메타 태그 분리, 번들 분리가 어렵고 조건문 필요 게시판별 lazy-load 및 SEO 메타 설정이 쉬움
개발 초기 비용 가장 빠르게 개발 가능 구조화에 약간의 시간 필요
파일 구조 훅 1개, Board 1개 HOC + Board + 필요 시 게시판별 UI 분리

처음엔 게시판이 3개뿐이라 커스텀 훅으로도 충분하다고 생각했지만, 게시판별로 기능이 점점 다르게 진화할 가능성을 고려해 구조적인 확장성과 유지보수의 장점을 가진 HOC 을 도입하기로 결정했습니다.

HOC(고차 컴포넌트)란?

HOC(Higher Order Component)는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수. HOC은 컴포넌트를 새로운 컴포넌트로 반환함.

🔎 HOC을 적용해보자!

구현한 HOC 구조 흐름은 다음과 같습니다.

  1. withBoard 함수는 컴포넌트(WrappedComponent)를 인자로 받습니다.
  2. 그리고 새로운 컴포넌트 함수(WithBoardComponent)를 반환합니다.
  3. 이 새로운 컴포넌트 함수(WithBoardComponent)는 렌더링될 때 - 필요한 로직을 처리합니다. (URL 파라미터 추출, 유효성 검사 등) - 그 후 원래의 WrappedComponent 에 추출한 게시판 정보를 props로 전달하여 렌더링합니다.
// withBoard.tsx

import { ComponentType } from "react";
import { BOARD_CONFIG } from "../constant/boardConfig";
import { useParams } from "react-router-dom";

// 래핑된 컴포넌트에 전달될 props 인터페이스 정의
export interface BoardProps {
  boardKey: string;
  boardTitle: string;
}

// withBoard HOC: 게시판 관련 로직을 분리하여 컴포넌트에 주입하는 고차 컴포넌트
export const withBoard = (WrappedComponent: ComponentType<BoardProps>) => {
  // 새로운 컴포넌트를 반환하는 HOC 패턴(DevTools에서 식별 가능하도록 내부 컴포넌트는 기명 함수로 구현)
  return function WithBoardComponent() {
    // useParams 통해 패스파라미터에서 boardName 추출
    const { boardName } = useParams<{ boardName: string }>();

    // 유효하지 않은 게시판 이름 처리
    if (!boardName || !(boardName in BOARD_CONFIG)) {
      return <div>존재하지 않는 게시판입니다.</div>;
    }

    // 해당 게시판의 제목 가져오기
    const boardTitle = BOARD_CONFIG[boardName as keyof typeof BOARD_CONFIG];

    // 원래 컴포넌트에 필요한 props를 전달하여 렌더링
    return <WrappedComponent boardKey={boardName} boardTitle={boardTitle} />;
  };
};
// BoardPage.tsx

import React from "react";
import Board from "../components/Board";
import { withBoard } from "../hoc/withBoard";

// React.memo를 사용해 불필요한 리렌더링 방지
const MemoizedBoard = React.memo(Board);

// HOC 적용해 내보내기
export default withBoard(Board);
// App.tsx

import { Route, Routes } from "react-router-dom";
import Header from "./components/Header";
import Home from "./pages/Home";
import BoardPage from "./pages/BoardPage";
import "./css/tailwind.css";
import "./css/custom.css";

function App() {
  return (
    <div>
      <Header />
      <main>
        <Routes>
          <Route path="/" element={<Home />} />
          {/* boardName 파라미터를 통해 각 게시판 접근 가능 */}
          <Route path="/:boardName" element={<BoardPage />} />
        </Routes>
      </main>
    </div>
  );
}

export default App;


결과적으로 Board라는 하나의 컴포넌트는 재사용하면서도 게시판 관련 로직은 HOC로 분리해 관리할 수 있도록 구조를 개선했습니다. 이를 통해 단일 책임 원칙과 확장성 모두를 충족시킬 수 있었습니다.

🙌 결론

리액트에서는 상황에 따라 커스텀 훅, 고차 컴포넌트(HOC), 컴포지션 등 다양한 패턴을 유연하게 활용할 수 있습니다. 만약 컴포넌트가 진화할 가능성이 있다면 UI 단위로 감쌀 수 있는 HOC도 좋은 선택지가 될 수 있지 않을까요?

댓글남기기