선언적 프로그래밍이란?
선언적 프로그래밍이란 무엇을 표현할지에 집중하고 어떻게 할지는 숨기는 개발 스타일이다.
UI를 선언적으로 사용할 수 있도록 만들면 당장 몰라도 되는 디테일은 숨겨지고 핵심 정보가 들어나서 로직을 파악하기 쉬워진다.
인터페이스부터 생각하자
리액트에서 간단한 alert 컴포넌트를 모달로 띄운다고 가정해보자. 사용처에서 UI 컴포넌트를 import하거나 만들고 isOpen과 같은 상태로 여닫기를 관리해줘야한다.
이에 비해 브라우저 내장 Web API인 window.alert()의 사용 방법은 간단하고 선언적이다.
alert("Hello world!");
어디서나 메시지와 함께 alert 함수를 호출해주기만 하면 창을 띄워준다.
이렇게 간단한 방식으로 리액트 컴포넌트도 띄울 수 있도록 만들어보자.
const Component = () => {
const [openAlert] = useAlert();
const handleClick = () => {
openAlert({
message: '경고!!',
});
}
return <button onClick={handleClick}>위험버튼</button>;
}
어떻게 만들까?
(Next.js의 app route 환경)
결론부터 말하면 body 태그 바로 아래에 컴포넌트를 두고 전역 상태관리를 통해서 열고 닫기를 조정해주면 된다.
몇가지 준비물이 있다.
- 전역에서 상태를 가지고있는 alertStore
- 상태를 관리해줄 useAlert 훅
- 실제로 보여질 alert 컴포넌트
- layout에서 alert를 보여줄지 결정하고 상태를 전달할 alertWrapper
가벼운 상태를 관리할 스토어만 필요해서 간단히 사용할 수 있는 jotai를 이용해서 스토어를 만들었다.
import { atom } from 'jotai';
export interface AlertProps {
message: string;
}
export const alertAtom = atom<AlertProps | null>(null);
상태를 핸들링할 함수를 포함하는 훅을 만든다. setAlert를 직접 노출하지 않아야 안정성이 올라간다.
import { type AlertProps, alertAtom } from '@/stores/alertStore';
import { useAtom } from 'jotai';
export const useAlert = () => {
const [alert, setAlert] = useAtom(alertAtom);
const openAlert = (alert: AlertProps) => {
setAlert(alert);
};
const closeAlert = () => {
setAlert(null);
};
return { alert, openAlert, closeAlert };
};
Alert UI를 만들어준다.
import { type AlertProps } from '@/stores/alertStore';
interface Props extends AlertProps {
onClick: () => void
}
export const Alert = ({ message, onClick }: Props) => {
return (
<div>
<p>{message}</p>
<button onClick={onClick}>닫기<button/>
</div>
)
};
마지막으로 래퍼 컴포넌트를 만들고 레이아웃에서 body에 놓아준다.
AlertWrapper에서 alert 전역 상태가 존재하면 Alert 컴포넌트를 렌더링 하도록 만든다.
'use client';
import { Alert } from '@/components/@common/alert/Alert';
import { useAlert } from '@/components/@common/alert/useAlert';
export const AlertWrapper = () => {
const { alert, closeAlert } = useAlert();
if (!alert) return null;
return <Alert message={alert.message} onClick={closeAlert} />;
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<body>
{children}
<AlertWrapper />
</body>
</html>
);
}
이제 어디서든 useAlert 훅의 openAlert 함수를 메시지와 함께 호출하면 WebAPI 처럼 선언적으로 alert 창을 띄울 수 있다.
사용처에서는 세부 구현을 몰라도 무엇을 하는지 직관적으로 알 수 있어서 로직의 파악이 쉬워졌다.
const DemoComponent = () => {
const [openAlert] = useAlert();
const handleClick = () => {
openAlert({
message: '경고!!',
});
}
return <button onClick={handleClick}>위험</button>;
};
confirm의 경우
alert와 비슷한 방식으로 confirm 모달도 선언적으로 사용할 수 있다.
특히 confirm 모달은 사용자로부터 확인 또는 취소의 응답을 받아야 하므로, Promise를 사용해 비동기적으로 결과를 반환하도록 설계하면 코드를 선형적으로 읽을 수 있어서 가독성이 높아진다.
const DemoComponent = () => {
const { openConfirm } = useConfirm();
const handleClick = async () => {
const confirmed = await openConfirm({
message: '정말로 삭제하시겠습니까?',
});
if (confirmed) {
// 삭제 로직
}
};
return <button onClick={handleClick}>삭제</button>;
};
confirm의 전역 상태와 훅을 만들어보자.
import { atom } from 'jotai';
export interface ConfirmProps {
message: string;
onConfirm: () => void;
onCancel: () => void;
}
export const confirmAtom = atom<ConfirmProps | null>(null);
useConfirm 훅을 만들어 상태를 핸들링하고 Promise를 반환한다.
import { useAtom } from 'jotai';
import { confirmAtom, type ConfirmProps } from '@/stores/confirmStore';
export const useConfirm = () => {
const [confirm, setConfirm] = useAtom(confirmAtom);
const openConfirm = (message: string) => {
return new Promise<boolean>((resolve) => {
setConfirm({
message,
onConfirm: () => resolve(true),
onCancel: () => resolve(false)
});
});
};
const closeConfirm = () => {
setConfirm(null);
};
return { confirm, openConfirm, closeConfirm };
};
Confirm UI 컴포넌트를 작성하자.
import { type ConfirmProps } from '@/stores/confirmStore';
export const Confirm = ({ message, onConfirm, onCancel }: ConfirmProps) => {
return (
<div>
<p>{message}</p>
<button onClick={onConfirm}>확인</button>
<button onClick={onCancel}>취소</button>
</div>
);
};
ConfirmWrapper를 작성하여 전역 상태에 따라 Confirm 컴포넌트를 렌더링하도록 한다.
"use client";
import { Confirm } from "@/components/@common/confirm/Confirm";
import { useConfirm } from "@/components/@common/confirm/useConfirm";
export const ConfirmWrapper = () => {
const { confirm, closeConfirm } = useConfirm();
if (!confirm) return null;
return (
<Confirm
message={confirm.message}
onConfirm={() => {
confirm.onConfirm();
closeConfirm();
}}
onCancel={() => {
confirm.onCancel();
closeConfirm();
}}
/>
);
};
RootLayout에 ConfirmWrapper를 추가한다.
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<body>
{children}
<AlertWrapper />
<ConfirmWrapper />
</body>
</html>
);
}
결론
이제 세부 구현에 대해 신경 쓰지 않고도 어디서든 간단하게 모달을 호출할 수 있게 됐다.
추상화와 선언적 프로그래밍을 통해서 원하는 로직을 빠르게 찾을 수 있는 코드를 만들고 빠르게 퇴근해보자!!