Dev Diary

Context API로 너무 많은 props drilling 개선하기 본문

Trouble Shooting

Context API로 너무 많은 props drilling 개선하기

sik9252 2024. 10. 9. 17:26
SMALL

* 여기서 구현한 RegisterAlarmDialog는 후에 SimpleAlarmDialog 라는 더 큰 범위의 공통 컴포넌트로 교체되었다.

 

진행중인 프로젝트에서 Dialog(모달창)를 사용할 일이 많았다. 그래서 useDialog()라는 hook을 만들어 여러 곳에서 재사용 할 수 있도록 하려고 했다. 그래서 아래와 같은 코드를 구현했는데...

import { useState, useCallback } from "react";

const useDialog = () => {
  const [isDialogOpen, setIsDialogOpen] = useState(false);

  const open = useCallback(() => setIsDialogOpen(true), []);
  const close = useCallback(() => setIsDialogOpen(false), []);

  return {
    isDialogOpen,
    open,
    close,
  };
};

export { useDialog };

 

문제점 발생

구현한 코드를 막상 사용해보니 결국엔 useRegister -> register -> registerAlarmDialog로 Dialog 상태에 대한 props를 계속 전달해 주어야 했다.

 

여러 곳에서 useDialog()를 가져다가 계속 쓸 수 있다는 점은 좋았지만, 결국엔 많은 props drilling이 발생하여 그냥 Dialog를 사용할 각각의 컴포넌트 안에 const [isErrorDialogOpen, setIsErrorDialogOpen] = useState("false"); 라는 상태를 하나 만들어 사용하는 것과 뭐가 다른지 큰 장점을 느끼지 못했다.

 

개선 방법으로 전역 상태가 떠올랐지만, 처음 이 프로젝트를 시작할 때 react query를 이용해 서버 상태를 관리하는 것만으로도 충분히 구현할 수 있지 않을까 하는 생각으로 이번에는 Recoil, Jotai와 같은 전역 상태를 정말 필요한 것이 아닌 이상 사용을 지양해보자라는 생각을 가지고 시작했기 때문에 어떻게 해야할지 고민했다.

 

고민 끝에 간단하게 Context API를 사용해보기로 했다. DialogProvider를 생성해 Provider로 감싸진 컴포넌트들은 Dialog의 상태를 전역적으로 사용할 수 있게 해주었다.


 

1. DialogProvider를 생성하고 필요한 컴포넌트를 감싸기

// dialogProvider.tsx

"use client";

import React, { createContext, useContext, useState, useCallback } from "react";

interface DialogContextProps {
  isDialogOpen: boolean;
  dialogOpen: () => void;
  dialogClose: () => void;
}

const DialogContext = createContext<DialogContextProps | undefined>(undefined);

const useDialogContext = () => {
  const context = useContext(DialogContext);
  if (!context) {
    throw new Error("useDialogContext는 DialogProvider 내부에서 사용되어야 합니다.");
  }
  return context;
};

const DialogProvider = ({ children }: { children: React.ReactNode }) => {
  const [isDialogOpen, setIsDialogOpen] = useState(false);

  const dialogOpen = useCallback(() => setIsDialogOpen(true), []);
  const dialogClose = useCallback(() => setIsDialogOpen(false), []);

  return <DialogContext.Provider value={{ isDialogOpen, dialogOpen, dialogClose }}>{children}</DialogContext.Provider>;
};

export { useDialogContext, DialogProvider };
// layout.tsx

import type { Metadata } from "next";
import "../styles/globals.css";
import { DialogProvider, QueryClientProvider, UserSessionProvider } from "@/providers";
import { Header } from "@/components/_shared/layout";

export const metadata: Metadata = {
  title: "문제저장소",
  description: "수능 문제저장소 입니다",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ko">
      <body>
        <QueryClientProvider>
          <UserSessionProvider>
            {/* DialogProvider로 감싸준다 */}
            <DialogProvider>
              <div className="w-screen">
                <Header isLogin={false} isAlreadyCreator={false} />
                {children}
              </div>
            </DialogProvider>
          </UserSessionProvider>
        </QueryClientProvider>
      </body>
    </html>
  );
}

 

2. Dialog를 사용할 곳에 context를 불러와 사용하기

// register.tsx

"use client";

import { useEffect } from "react";
import { useDialogContext } from "@/providers";

const useRegister = () => {
  // context를 가져와서 사용
  const { dialogOpen } = useDialogContext();
  
  ...생략

  useEffect(() => {
    if (registerError) {
      dialogOpen();
    }
  }, [dialogOpen, registerError]);

  return {
    handleRegister,
    registerError,
    isRegisterPending,
  };
};

export { useRegister };
// register.tsx

"use client";

import React from "react";
import { RegisterAlarmDialog } from "../../components";

const Register = () => {
  return (
    <>
      <div className="space-y-[32px] w-[620px] m-auto">

		... 생략
      
      {/* props로 dialog 관련 상태를 내려줄 필요가 없어짐 */}
      <RegisterAlarmDialog message={registerError?.message} />
    </>
  );
};

export { Register };
// registerAlarmDialog.tsx

import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/_shared/ui";
import { useDialogContext } from "@/providers";
import React from "react";

interface RegisterAlarmDialogProps {
  message: string | undefined;
}

const RegisterAlarmDialog = React.memo(({ message }: RegisterAlarmDialogProps) => {
  // props로 계속 내려줄 필요 없이 context를 가져와서 사용
  const { isDialogOpen, dialogClose } = useDialogContext();

  return (
    <Dialog open={isDialogOpen} onOpenChange={dialogClose}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>알림</DialogTitle>
          <DialogDescription>{message}</DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="navy" onClick={dialogClose}>
              확인
            </Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
});

RegisterAlarmDialog.displayName = "RegisterAlarmDialog";

export { RegisterAlarmDialog };

 

결과적으로 3~4번 들어가야 했던 props를 제거할 수 있게 되었다. 하지만, 아직 구현할 기능들이 많은데 나중가서 진짜 전역 상태 관리 도구를 도입해야하는게 아닐까 하는 고민을 더 하게 될 수도 있다는 생각이 들었다... 있으면 편하긴 한데 안쓰고 성공할 수 있을까?

LIST