본문 바로가기
project/google-calendar-clone

React에서 IoC 활용하기

by qjatjs123123 2025. 6. 8.

 

안녕하세요!


구글 캘린더를 클론 코딩하며 유연하고 확장 가능한 구조를 고민해봤습니다.

강하게 결합된 구조보다는 외부로부터 주입받아 사용하는 느슨한 결합 방식, 즉 Ioc (제어의 역전)을 사용했습니다.

 

많은 관심 부탁드립니다.

 

 


 

 

IoC이란?

제어의 흐름을 개발자가 아닌 외부로 넘기는 설계 원칙입니다.
전통적으로 컴포넌트가 직접 의존성을 생성하거나 처리했다면, IoC에서는 필요한 것들을 외부에서 주입받아 사용합니다.

 

코드 간 결합도를 낮추고, 더 유연하고 확장 가능한 구조를 만들 수 있습니다.

 

 

 


 

 

컴포넌트 주입

이 방법은 리액트에서 가장 간단한 IoC 구현 방식으로, 컴포넌트가 자신의 내부 로직 대신 외부에서 전달받은 UI를 렌더링하도록 제어권을 넘깁니다.

 

 

props로 컴포넌트를 전달하는 방법은 크게 두 가지가 있습니다:
props 을 사용하는 방법,  children 로 전달하는 방법입니다.

 

 


 

 

(props로 컴포넌트 전달)

props로 컴포넌트를 전달할 때는 주로 특정 슬롯이나 위치에 맞춰 UI를 구성할 때 사용합니다.

 

어떤 UI 컴포넌트가 어느 위치에 들어갈지 명확하게 나타낼 수 있고, 넘겨주는 컴포넌트에 따라 다른 화면 구성을 할 수 있기 때문입니다.

 

 

여기서 레이아웃을 잡을 때 사용했습니다.

const Layout = ({ header, sidebar, children }: BaseLayoutProps) => {
  return (
    <div className="">
      <header className="h-[64px]">{header}</header>
      <div className="flex h-[calc(100vh-64px)]">
        <div className="flex flex-col w-[256px] bg-transparent h-[100%]">
          {sidebar}
        </div>
        <main className="shadow-md bg-white flex-1 rounded-[20px] mb-[16px]">
          {children}
        </main>
        <aside className="w-[56px] bg-transparent">
          <RightSideMenu />
        </aside>
      </div>
    </div>
  );
};

export default Layout;



// 사용 컴포넌트
const Calendar = () => {
  const mode = useSelector((state: RootState) => state.mode.mode);

  return (
    <Layout
      header={
        <HeaderProvider>
          <Header />
        </HeaderProvider>
      }
      sidebar={<SideBar />}
    >
      {mode !== MODE.MONTH ? <TodoWithRowCells /> : <TodoWithCell />}
    </Layout>
  );
};

 

구글 캘린더의 Layout은 크게 Header, Sidebar, Content 영역으로 구성됩니다.


이러한 레이아웃 UI 요소들을 각각 props로 전달하면, 원하는 콘텐츠를 각 영역에 유연하게 배치할 수 있습니다.

 

 


 

 

(children으로 컴포넌트 전달)

children은 컴포넌트를 전달할 땐, 주로 컴포넌트 내부에 하나의 주된 콘텐츠를 감싸서 전달할 때 주로 사용합니다.

 

 

구글 캘린더 데이트 피커

여기서는 DatePicker 컴포넌트에 children으로 Title, ContentHeader, Content를 감싸서 전달합니다.
이렇게 하면 DatePicker는 레이아웃만 담당하고, 실제 표시할 내용은 외부에서 자유롭게 구성할 수 있습니다.

 

 

만약 "일, 월, 화, ... 토"와 같은 요일이 없는 형태의 DatePicker가 요구된다면, ContentHeader 컴포넌트를 전달하지 않으면 됩니다.
또한 Title 영역의 UI를 바꾸고 싶다면, 기존 Title 대신 원하는 컴포넌트를 children으로 전달해 교체할 수 있습니다.

 

 

 

코드

const SideBar = () => {
  return (
    <>
      <DatePickerProvider>
        <DatePicker>
          <DatePicker.Title />
          <DatePicker.ContentHeader />
          <DatePicker.Content />
        </DatePicker>
      </DatePickerProvider>
      <Footer/>
    </>
  );
};

export default SideBar;

 

 

 


 

 

 

합성 컴포넌트 패턴(Compound Component Pattern)

합성 컴포넌트 패턴은 리액트의 Context/Provider를 사용하여 여러 종류의 컴포넌트가 하나의 로직을 공유할 수 있게 하는 방법입니다.

 

사실 위에 DatePicker도 합성 컴포넌트 패턴으로 개발했는데요.

 

 

DatePicker의 구성요소별로 별개의 컴포넌트로 분리하였습니다.

 

이로서유연하게 컴포넌트의 UI를 컨트롤할 수 있고, 어떠한 UI 변경 사항이 발생하더라고 손쉽게 해결할 수 있게 되었습니다.

 

또한 각각의 분리된 컴포넌트들은 Datepicker라는 도메인에 더 이상 종속되지 않고 각각 본연의 기능과 역할(SRP: Single Responsibility Principle)로도 사용할 수 있게 되었습니다.

 

 

 

만약 Content.Data 부분에 디자인 요청이 들어온다면,

기존 코드는 전혀 수정하지 않고 해당 부분만 새로운 컴포넌트로 교체하면 됩니다.


마찬가지로 ContentHeader 영역에 대한 디자인 변경 요청이 있어도,

기존 구조는 그대로 둔 채 변경된 UI 컴포넌트만 교체하면 됩니다.

 

 

(Title, Content, ContentHeader)

const Title: FC<DatePickerTitleProps> = () => {
  const { left, right } = useDatePicker();

  return (
    <div className="grid gap-2 grid-cols-7 items-center mb-4">
      <div className="col-span-5 pl-2">{left}</div>
      <div className="col-span-2 flex justify-end">{right}</div>
    </div>
  );
};
const Content: ContentComponent = ({ children }) => {
  const { days } = useDatePicker();

  return (
    <div className="grid grid-cols-7 gap-2 place-items-center">
      {children || days.map((day, idx) => <Content.Data key={idx} day={day} />)}
    </div>
  );
};

const ContentHeader: ContentHeaderComponent = ({ children }) => {
  const { weekDays } = useDatePicker();

  return (
    <div className="grid grid-cols-7 gap-2 text-center font-semibold mb-2">
      {children || weekDays.map((dayName, idx) => (
        <ContentHeader.Data key={idx} dayName={dayName} />
      ))}
    </div>
  );
};

 

 

(ContentData)

const ContentData: FC<ContentDataProps> = ({ day }) => {
  const { selectedDate, setDate, now } = useDatePicker();

  const isToday = day === now;
  const isSelected = day === selectedDate;
  const isCurrentMonth = dayjs(day).month() === dayjs(selectedDate).month();

  const type = isToday ? "primary" : isSelected ? "light" : "none";
  const textColor = isToday ? "white" : isCurrentMonth ? "black" : "gray";

  return (
    <Button
      onClick={() => setDate(day)}
      type={type}
      className="text-center rounded-full w-[24px] h-[24px] flex items-center justify-center text-sm"
    >
      <Text
        size="xs"
        color={textColor}
        weight={isToday ? "bold" : "normal"}
      >
        {dayjs(day).format("D")}
      </Text>
    </Button>
  );
};

Content.Data = ContentData;

 

 

(ContentHeaderData)

const ContentHeaderData: FC<ContentHeaderDataProps> = ({ dayName }) => {
  return (
    <div className="text-gray-600 text-center">
      <Text size="xs">{dayName}</Text>
    </div>
  );
};

ContentHeader.Data = ContentHeaderData;

 

 

정리하자면 합성 컴포넌트는

 

각 컴포넌트를 분리하고, 어떠한 UI 변경사항이 오더라도 기존 코드는 수정없이 손쉽게 해결할 수 있다는 장점이 있습니다. 그리고 페이지 내 수많은 데이터가 한눈에 읽히는 장점이 있습니다.

 

반면, 합성 컴포넌트만을 사용하는 것이 좋다고만 볼 수도 없습니다. 추가적으로 Context/Provider 패턴을 구성해야 하며, 불필요한 렌더링이 발생할 수도 있습니다.

'project > google-calendar-clone' 카테고리의 다른 글

커스텀 훅으로 로직 재사용하기  (0) 2025.06.08
Atomic 디자인  (0) 2025.06.07