토스트(Toast)는 팝업의 일종으로, 이름처럼 토스터기에서 빵이 튀어나오는 것과 비슷한 형태의 UI 요소입니다. 이번 프로젝트에서 서버 오류가 발생했을 때, 서버의 오류를 매번 확인하는 것이 번거로워 직접 토스트 UI를 구현하려 했습니다.
이전에 만들어진 토스트 컴포넌트가 있지만, 몇 가지를 개선하고자 했습니다.
디자인 패턴은 소프트웨어 공학에서 자주 발생하는 설계 문제에 대해 검증된 해결 방법을 제공하는 설계 방법론입니다. 제가 원하는 토스트를 구현하기 위해 디자인 패턴 중 옵저버 패턴(Observer Pattern)을 적용해 보았습니다.
옵저버 패턴(Observer Pattern)은 Gang of Four(GoF) 디자인 패턴 중 하나로, 특정 주제(Subject)를 여러 관찰자(Observer)가 구독하는 방식입니다. 주제의 상태가 변경되면, 구독 중인 모든 관찰자에게 즉각적으로 업데이트가 전달됩니다.
쉽게 말해, 여러 컴포넌트가 동일한 주제를 구독하고, 주제의 상태가 변경될 때 각 컴포넌트가 이를 즉시 반영하도록 하는 패턴입니다.
옵저버 패턴의 이점:
Subject
와 변경을 감지하는 Observer
간의 관계를 느슨하게 유지할 수 있습니다. (느슨한 결합)Subject
// 부모 클래스
class Subject<T> {
private observers: T[] = [];
subscribe(observer: T) {
this.observers.push(observer);
}
unsubscribe(observer: T) {
this.observers = this.observers.filter(o => o !== observer);
}
notify<U>(params: U) {
this.observers.forEach(observer => {
if (typeof observer === 'function') {
observer(params);
}
});
}
}
Subject
클래스는 옵저버 패턴 구현을 위한 일반화된 부모 클래스입니다.
옵저버(구독자)를 관리하고, 옵저버에게 변경 사항을 알리는 두 가지 역할을 합니다. Toast
모듈에만 종속되지 않고, 다양한 상황에 유연하게 확장할 수 있도록 제너릭 타입을 활용했습니다.
Subject
객체의 상태 변화를 관찰(구독)할 옵저버를 추가합니다.Subject
객체의 상태 변화를 구독하고 있는 특정 옵저버를 제거(구독 해제)합니다.Subject
객체의 상태 변화를 구독하고 있는 옵저버들에게 변화를 알립니다.export interface ToastObserverParam {
message: string;
type: 'error' | 'success';
}
// 자식 클래스
export class ToastSubject extends Subject<(params: ToastObserverParam) => void> {
private static instance: ToastSubject;
private constructor() {
super();
}
public static getInstance(): ToastSubject {
if (!ToastSubject.instance) {
ToastSubject.instance = new ToastSubject();
}
return ToastSubject.instance;
}
}
Subject
를 상속받은 ToastSubject
는 타입을 구체화하며, 애플리케이션 전체에서 하나의 인스턴스만 존재함을 보장하는 싱글톤 패턴으로 구현되었습니다.
const toastSubject = ToastSubject.getInstance();
// apiClient는 Axios 인스턴스
apiClient.interceptors.response.use(
(response) => { // 성공 응답 가로채기
return response;
},
(error) => { // 실패 응답 가로채기
toastSubject.notify({
message: `Server Error: ${error.response.status}`,
type: 'error',
});
return Promise.reject(error);
}
);
현재 구현한 토스트의 목적은 서버 오류 발생 시 이를 사용자에게 알리는 것입니다. Axios의 interceptors
를 활용하여 API 응답을 감지하고, 에러가 발생했을 때 toastSubject.notify
를 통해 옵저버들에게 메시지를 전달합니다.
Toast
컴포넌트이제 ToastSubject
를 구독할 ToastObserver 컴포넌트 입니다.
// ToastsObserver.tsx
export interface IToast extends ToastObserverParam {
id: number;
}
const ToastsObserver = () => {
const [toasts, setToasts] = useState<IToast[]>([]);
useEffect(() => {
const toastSubject =
ToastSubject.getInstance();
const handleNewToast = (params: ToastObserverParam) => {
const id = Date.now();
setToasts((prev) => [
...prev,
{
id,
...params,
},
]);
setTimeout(() => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, 5000);
};
toastSubject.subscribe(handleNewToast);
return () => {
toastSubject.unsubscribe(handleNewToast);
};
}, []);
if (!toasts || toasts.length < 1) {
return null;
}
return createPortal(
<ToastContainer>
{toasts.map((toast) => (
<Toast
key={toast.id}
type={toast.type}
role="alert"
aria-live="assertive"
>
{toast.message}
</Toast>
))}
</ToastContainer>,
document.body
);
};
handleNewToast
함수가 구독자 배열에 추가됩니다. (언마운트 시, 클린업으로 구독 해제)params
(메시지)를 전달하여 이벤트를 발생시킵니다. (상단의 Axios 인터셉터에서 발생)setToasts
로 toasts
상태에 새로운 토스트가 추가되고, 5초간 유지됩니다.toasts
가 추가되면 React.createPortal
을 사용해 document.body
에 새로운 노드를 생성합니다.💡 React.createPortal ?
React 프로젝트에서 컴포넌트는public/index.html
의<div id="root">
에 렌더링됩니다. 하지만ReactDOM.createPortal()
을 사용하면, 다른 DOM 트리에 컴포넌트를 렌더링할 수 있습니다. 이를 통해 CSS 작업에서 다른 컴포넌트로부터 연쇄적으로 영향을 받는 것을 피할 수 있습니다.
마지막으로 ToastsObserver
컴포넌트를 애플리케이션의 최상단에 위치시키기만 하면 설정이 완료됩니다.
// main.tsx
root.render(
<StrictMode>
<GlobalStyles />
<App />
<ToastsObserver />
</StrictMode>
);
서버 에러가 아닌 다른 곳에서도 토스트가 필요할 때 toastSubject
의 notify
메서드를 호출하기만 하면 동일하게 사용할 수 있습니다.
데이터 페칭에 실패할 때마다 서버 오류 알람이 잘 렌더링 됩니다! 디자인 패턴은 일반적으로 객체 지향 프로그래밍(OOP)에서 많이 사용되지만, 프론트엔드에서도 이를 활용하여 코드 구조를 체계적으로 관리하고, 반복되는 문제에 대한 이미 검증된 해결 방법을 사용할 수 있습니다.