이번 프로젝트를 진행하며 Toast Component를 구현하게된 이유는 다음과 같습니다.
라이브러리 의존도를 줄이기 위해
프로젝트를 진행하면서, 작은 기능들조차 외부 라이브러리에 의존하는 것이 학습적인 측면에서 올바르지 않다고 생각했습니다. 특히, Toast와 같은 간단한 기능에 경우 자체적으로 구현하는 것이 프로젝트의 관리 측면에서 너 나은 접근 방법이라 생각했으며 이를 통해 불필요한 라이브러리 번글 크기 증가와 의존성 문제를 방지하고자 했습니다.
옵저버 패턴 학습 기회
챌린지 기간 학습했던 옵저버 패턴을 실제 코드로 구현해보며, 이 패턴이 React 환경에서 어떻게 활용될 수 있는지 경험해보고 싶었습니다. Toast는 발행-구독 방식의 상태 관리가 필요한 특성을 가지고 있어, 옵저버 패턴을 적용하기에 용이하다고 판단했습니다. 이를 통해 패턴의 이해도를 높이고, 다양한 방식으로 상태를 관리할 수 있는 방법을 탐구할 기회를 얻고자 했습니다.
Context API나 전역 상태를 사용하지 않고, 최소한의 상태 관리 구현
전역 상태 관리 도구나 Context API는 강력하지만, 작은 범위의 상태 변화만 필요한 경우 불필요하게 복잡해질 수 있습니다. Toast 컴포넌트에서는 전역 상태 없이 독립적으로 작동하는 상태 관리 방식을 탐구하고자 했습니다. 이를 통해, 작은 범위의 상태 변화에만 반응하고, 필요한 최소한의 렌더링만 발생시키는 방식으로 동작하도록 설계하는 데 집중했습니다.
전체적인 구조와 흐름은 다음과 같습니다.
ToastContainer
는 커스텀 훅인 useToastContainer
훅을 호출합니다. 이 훅이 마운트되었을 때, EventManager의 show
이벤트에 buildToast
함수를 등록해줍니다.toast
함수를 호출하면 EventManager의 show
이벤트를 emit 해줍니다.buildToast
함수가 호출되어, toastToRender
에 새로운 토스트 메시지가 추가됩니다.ToastContainer
는 getToastToRender
함수를 통해 toastToRender
Map에 있는 토스트 메시지 리스트를 사용자가 볼 수 있도록 렌더링해줍니다.ToastTranslation
내부에서 관리합니다.Toast
메시지 관련 비즈니스 로직을 구현한 커스텀 훅입니다. 등록된 Toast
들의 id를 제외한 정보는 ref를 통해 관리하여 불필요한 리렌더링을 방지하고 만약 ref를 통해 변경된 정보를 통해 렌더링이 필요하다면 forceUpdate
를 통해 렌더링을 진행합니다.
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const [toastIds, setToastIds] = useState<ToastId[]>([]);
const containerRef = useRef(null);
const toastToRender = useRef(new Map<ToastId, Toast>()).current;
const instance = useRef<ContainerInstance>({
props,
count: 0,
toastKey: 1,
displayedToast: 0,
getToast: (id) => toastToRender.get(id),
}).current;
const buildToast = (content: ToastContent, options: NotValidatedToastProps) => {
// ...
const deleteToast = () => {
toastToRender.delete(toastId);
instance.count -= 1;
if (instance.count < 0) instance.count = 0;
forceUpdate();
};
// ...
};