개발새발 로그

[2024-02-08] 팀 프로젝트 개인회고 - set함수에 함수를 인수로,클래스형컴포넌트를 함수형 컴포넌트로 본문

TIL

[2024-02-08] 팀 프로젝트 개인회고 - set함수에 함수를 인수로,클래스형컴포넌트를 함수형 컴포넌트로

이즈흐 2024. 2. 8. 15:13

1. Class형 컴포넌트를 함수형 컴포넌트로!

나는 Toast 컴포넌트를 만들면서 Class형 컴포넌트로 먼저 개발을 했다.

creatToast를 저장할 때 편할 것 같아서 사용했는데

모든 컴포넌트가 함수형 컴포넌트를 쓰는데 갑자기 클래스형을 쓰는 것이 이상했고,

클래스형 컴포넌트가 메모리 자원을 함수형 컴포넌트보다 더 사용,

클래스형 컴포넌트는 빌드 후 파일 크기가 함수형 컴포넌트보다 더 큼

React 공식문서에서 함수형 컴포넌트를 권장 등의 이유로 리팩토링을 결정했다.

 

정확한 함수형 컴포넌트의 장점은 아래와 같다

  • 리랜더링 될 때의 값을 유지합니다. 즉, immutable 하다는 것. (위의 예제참고)
  • 함수형 컴포넌트는 props에 따른 랜더링 결과를 보장받습니다.
    • immutable한 props를 받기 때문에 결국엔 랜더링 결과가 보장된다는 것. 함수형 프로그래밍의 특징과 일맥상통함이 있습니다
  • 매개변수로 받는 props의 destructuring을 활용해 가독성을 보장받을 수 있습니다.
  • 함수의 모든 장점을 이용할 수 있습니다. (결국 함수니까)
  • 함수형 컴포넌트를 사용했을 때 코드가 간결해지고 가독성도 좋습니다!
class Toast {
  portal: HTMLElement | null = null;
  createToast: ToastCreate | undefined;

  constructor() {
    if (typeof window !== "undefined") {
      const portalId = "toast-portal";
      const portalElement = document.getElementById(portalId);

      if (portalElement) {
        this.portal = portalElement;
        return;
      }

      this.portal = document.createElement("div");
      this.portal.id = portalId;
      document.body.appendChild(this.portal);

      createRoot(this.portal).render(
        <ToastManager
          bind={(createToast: ToastCreate) => {
            this.createToast = createToast;
          }}
        />
      );
    }
  }

  show(message: string, iconId: ToastIconId, duration = 2000) {
    if (!this.createToast) throw new Error("ToastManager 초기화 오류");
    this.createToast(message, iconId, duration);
  }
}
const toastInstance = new Toast();
export default toastInstance;

위 코드가 리팩토링 이전의 코드다.

충분히 함수형 컴포넌트로 바꿀 수 있는 코드여서 리팩토링을 했다.

 

근데 리팩토링 도중에 문제가 생겼다.

createToast가 계속 빈값으로 할당되어 show 메서드를 실행할 때 에러를 발생하게 되는 것이다.

무슨 문제일까?

"use client";

import { createRoot } from "react-dom/client";

import ToastManager from "./ToastManager";
import { ToastCreate, ToastIconId } from "./type";
import { useEffect, useState } from "react";

const Toast = () => {
  const [portal, setPortal] = useState<HTMLElement | null>(null);
  const [createToast, setCreateToast] = useState<ToastCreate>();

  useEffect(() => {
    if (typeof window !== "undefined") {
      const portalId = "toast-portal";

      const portalElement = document.getElementById(portalId);
      if (portalElement) {
        setPortal(portalElement);
        return;
      } else {
        const newPortalElement = document.createElement("div");
        newPortalElement.id = portalId;
        document.body.appendChild(newPortalElement);

        setPortal(newPortalElement);
        createRoot(newPortalElement).render(
          <ToastManager
            bind={(createToast) => {
              setCreateToast(createToast); //이 부분
              console.log(createToast);
            }}
          />
        );
      }
    }
  }, []);

  const show = (message: string, iconId: ToastIconId, duration = 2000) => {
    if (!createToast) throw new Error("ToastManager 초기화 오류");
    createToast(message, iconId, duration);
  };

  return { show };
};

export default Toast;

위 코드를 실행했을 때 ToastManager에서 creatToast함수를 잘 갖고오고 있었다.

그래서 위 컴포넌트에서 관리하는 createToast 상태에 저장해야했는데 계속해서 빈값을 넣고 있었다.

 

사실 이 문제 해결방법은 아주 간단했다.

<ToastManager
    bind={(createToast) => {
      setCreateToast(() =>  createToast);
    }}
  />

주석으로 표기한 부분을 위처럼 바꾸면 해결되는 것이었다...

이게 왜 이렇게 되는걸까?

 

이걸 알기위해 간단한 실험을 해보았다.

...

const func = () => {};
setCreateToast(func);

...

이렇게 set함수에 func함수를 그대로 넣으면 어떻게 될까?

console.log(createToast);를 하게되면 아래와 같이나온다.

func함수는 void 형식이다.

내가 setCreateToast에 넣어줬던 함수도 ()=>void형식이었다.

 

그럼 다시 func을 업데이트 함수로넣어주면 어떻게 될까?

이렇게 함수 그대로가 createToast에 저장이된다.

set함수는 인수로 함수를 받으면 그 함수의 반환 값을 createToast에 저장한다고 한다.

이게 업데이트함수에서 적용되는 부분인데

우리는 set함수에 인수로 함수(createToast)를 넣었기때문에 createToast함수자체가 들어가는게 아니라

createToast 함수의 반환값이 들어가는 것이다

즉 void 함수니까 undefined가 들어가는 것이다.

 

이해가 안될 수 있다.

그럼 반환값이 있는 함수로 진행해보자

const func = () => {
  return 1;
};
setCreateToast(func);

이렇게 진행한다면 어떻게 될까?

그럼 createToast에는 1이 저장이된다.

즉 func의 반환 값이 저장이 된다.

이를 통해 위에서 추측한 것이 맞다고 예상된다.(정확하지 않을 수 있습니다.)

 

그럼 아까처럼 업데이트함수 () => 로 set함수에 넣으면 어떻게 될까?

 const func = () => {
  return 1;
};
setCreateToast(()=>func);

예상한대로 함수 그 자체가 들어가게 된다.

그래서 set함수에 함수를 넣을 때는 이 부분도 꼭 고려를 해야한다.

 

나처럼 만약 하위 컴포넌트에서 만들어진 함수를 콜백함수로 가져오려고 할 때는 이 점을 항상 유의해야한다!!

 

 

2. 처음부터 잘못된 나의 코드..싱글톤 패턴을 제대로 알자!

기존 클래스형 컴포넌트로 구현한 Toast는 싱글톤 패턴이다.

그래서 그걸 함수형으로 바꾸면서 잘 바꿨다고 생각했는데 문제가 있었다.

만약 const newToast = new Toast()를 어떤 컴포넌트에서 만들어서 show를 실행하고,

어떤 컴포넌트에서 const newToast2 = new Toast()를 해서 show를 실행하면 
toast가 하나의 인스턴스로 실행되는 것이아니고, 각자의 인스턴스를 생성해 toast가 겹치게 되는 문제가 생겼다.

 

그래서 다시 봐보니 createToast가 최초로 생성된 이후에도 새로운 인스턴스로 인정되어 각 다른 환경을 갖고있는 것이었다.

너무 당연한 문제였다.

그래서 아래와 같이 수정해줬다.

"use client";

import { createRoot } from "react-dom/client";
import ToastManager from "./ToastManager";
import { ToastCreate, ToastIconId } from "./type";
import { useEffect, useState } from "react";

let createToastInstance: ToastCreate | undefined;

const Toast = () => {
  const portalId = "toast-portal";
  const [hasPortal, setHasPortal] = useState(false);

  useEffect(() => {
    if (typeof window !== "undefined") {
      const portalElement = document.getElementById(portalId);
      if (!portalElement) {
        const newPortalElement = document.createElement("div");
        newPortalElement.id = portalId;
        document.body.appendChild(newPortalElement);

        createRoot(newPortalElement).render(
          <ToastManager
            bind={(createToast) => {
              createToastInstance = createToast;
              setHasPortal(true);
            }}
          />
        );
      } else {
        setHasPortal(true);
      }
    }
  }, []);

  const show = (message: string, iconId: ToastIconId, duration = 2000) => {
    if (!createToastInstance) throw new Error("ToastManager 초기화 오류");
    createToastInstance(message, iconId, duration);
  };

  return { show, hasPortal };
};

export default Toast;

let으로 creatToast를 저장하면 모듈 수준에서 관리되기 때문에 모든 컴포넌트에서 동일한 인스턴스를 공유하게 된다.

하지만 싱글톤패턴이 마냥 좋은 것이 아니라고 모두들 말하고 있다.

그래서 이 부분도 추후 어떻게하면 하나의 인스턴스를 공유하면서 사용할 수 있을 지 고민해봐야된다고 생각됐다.

 

728x90
반응형
LIST