브라우저 지원 함수인 alert
, confirm
, prompt
를 실행하면 다이얼로그가 뜨고 상호작용이 끝나서 다이얼로그가 닫히면 그 결과를 return 받을 수 있다.
이 다이얼로그 함수는 사용이 간단한 대신 동기 함수라는 점과 UI 커스텀이 어렵다는 단점이 있어 활용도는 낮은 편이다. 동기 함수이기 때문에 다이얼로그가 닫히기 전까지 자바스크립트 실행이 멈춰 있게 되고, UI 커스텀이 어려우니 사용자에게 일관적인 경험 제공이 어려워진다.
그래서 이를 해결하기 위해 직접 커스텀 다이얼로그를 만들어 사용하기도 하는데, 이번 포스팅에서는 어떻게 이 다이얼로그들을 React 컴포넌트로 만들 수 있을까 고민하고 만들어 본 내용을 공유하려 한다.
전체 코드는 여기서 보실 수 있습니다.
세 함수의 동작이 비슷하니 그 중에서 confirm
구현에 초점을 맞춰보자.
우리는 컴포넌트 안에서 아래 코드처럼 사용하고 싶다.
// confirm 함수 호출해서 Confirm 다이얼로그 띄우기
const confirmResult = confirm('are you sure?');
// 다이얼로그 결과 받기
console.log(confirmResult);
confirm()
호출 시 기대 동작은 다음과 같다.
Confirm 다이얼로그를 띄우기 위해서는 showDialog
같은 상태를 변경시키면 될 것 같고, 상호작용이 끝나는 시점에 결과를 return 하기 위해서는 Promise의 resolve
를 이용하면 될 것 같다.
const confirm = (message?: string) => new Promise((resolve) => {
// Confirm 다이얼로그 띄우기
// resolve 함수를 Dialog의 close 핸들러로 등록
});
이 함수는 Confirm 다이얼로그와 밀접한 관련이 있으므로 Confirm 다이얼로그를 관리하는 컴포넌트에서 confirm
함수를 제공하고, 하위 컴포넌트에서 호출하게 하면 될 것 같다. 하위 컴포넌트에서 자유롭게 confirm
함수를 사용할 수 있도록 함수는 Context API로 관리하자.
먼저 confirm
함수를 제공할 ConfirmContext
를 만들자.
type Type = {
confirm: (message?: string) => Promise<boolean>;
};
const ConfirmContext = createContext<Type>({
confirm: () => new Promise((_, reject) => reject()),
});
타입은 브라우저 지원 confirm
함수 처럼 다이얼로그에 표시할 message를 선택적으로 받고, 확인/취소 선택 여부에 따라 boolean 값을 리턴하도록 했다.
초기값은 외부에서 초기화를 하지 않으면 사용할 수 없도록 강제하기 위해 항상 실패하는 Promise를 만들어 넣었다.
ConfirmDialog
는 Confirm 다이얼로그를 제어하고 ConfirmContext
를 제공하는 핵심 컴포넌트이다.
type ConfirmState = {
message: string;
onClickOK: () => void;
onClickCancel: () => void;
};
const ConfirmDialog = ({ children }: { children: React.ReactNode }) => {
const [state, setState] = useState<ConfirmState>();
const confirm = (message?: string): Promise<boolean> => {
return new Promise((resolve) => {
// state를 변경해 Confirm 다이얼로그를 띄운다.
setState({
message: message ?? '',
onClickOK: () => {
// ok 클릭한 경우, 다이얼로그 닫고 true로 Promise 종료
setState(undefined);
resolve(true);
},
onClickCancel: () => {
// cancel 클릭한 경우, 다이얼로그 닫고 false로 Promise 종료
setState(undefined);
resolve(false);
},
});
});
};
return (
<ConfirmContext.Provider value={{ confirm }}>
{children}
{/* state 여부에 따라 Confirm 다이얼로그 띄우기 */}
</ConfirmContext.Provider>
);
};
Confirm 다이얼로그에 필요한 상태는 ConfirmState
로 구성해 한 번에 관리하게 했다.
이제 다이얼로그 UI를 담당하는 Confirm 컴포넌트를 만들자.
interface Props {
message: string;
onClickOK: () => void;
onClickCancel: () => void;
}
const Confirm = ({ message, onClickOK, onClickCancel }: Props) => {
return (
<div className="dialog-container">
<div className="dialog">
<h2 className="title">Confirm</h2>
<div className="text">{message}</div>
<div className="buttons">
<button onClick={onClickCancel}>cancel</button>
<button onClick={onClickOK}>ok</button>
</div>
</div>
</div>
);
};
Confirm
UI는 이후에 추가할 Alert
, Prompt
UI와 비슷하기 때문에 일반적인 구조로 만들었다.
이제 앞서 만든 ConfirmDialog
에서 이 Confirm
을 보여준다.
const ConfirmDialog = ({ children }: { children: React.ReactNode }) => {
...
return (
<ConfirmContext.Provider value={{ confirm }}>
{children}
{state && (
<Confirm
message={state.message}
onClickOK={state.onClickOK}
onClickCancel={state.onClickCancel}
/>
)}
</ConfirmContext.Provider>
);
}
이제 필요한 기능은 다 만들었으니 다이얼로그를 띄워보자!
먼저 최상위 컴포넌트 App.tsx
에 가서 ConfirmDialog
를 사용할 수 있도록 감싸 주었다.
function App() {
return (
<ConfirmDialog>
<Home />
</ConfirmDialog>
);
}
그러면 <ConfirmDialog>
의 하위 컴포넌트인 <Home>
에서는 confirm
함수를 아래처럼 사용할 수 있다!
const Home = () => {
const { confirm } = useContext(ConfirmContext);
const onConfirmClick = async () => {
const result = await confirm('are you sure?');
console.log(result);
};
return (
<main className="home">
<h1>Home</h1>
<button onClick={onConfirmClick}>다이얼로그 띄우기</button>
</div>
</main>
);
};
목표한 기능은 구현했고, 이제 추가적으로 브라우저 confirm
동작과 비슷하게 아래 기능을 넣어주려고 한다.
이 기능은 간단히 OK 버튼에 autoFocus
속성을 추가하면 된다.
<button onClick={onClickOK} autoFocus>ok</button>
<Confirm>
에서 keydown
이벤트를 받아서 ESC 키를 처리하자.
ESC로 다이얼로그를 닫는 것도 취소 동작으로 간주한다.
const Confirm = (...) => {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClickCancel();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [onClickCancel]);
...
};
다이얼로그를 제외한 화면 이벤트를 어떻게 막을 수 있을까 고민하다가 화면 전체를 덮는 overlay 요소 하나를 추가해서 overlay에서 이벤트를 다 먹어버리는 것으로 처리할 수 있겠다고 생각했다.
<Confirm>
에 overlay 요소를 하나 추가하고, 그 요소에서는 캡처링 단계에서 stopPropagation
을 이용해 이벤트가 하위 요소로 넘어가는 것을 막는다.
const Confirm = (...) => {
...
return (
<div className="dialog-container">
<div className="overlay" onClickCapture={(e) => e.stopPropagation()} />
<div className="dialog">
...
</div>
);
});
이렇게 브라우저 지원 confirm
과 비슷한 동작을 하는 React 컴포넌트 Confirm
을 만들어보았다! 같은 메커니즘으로 Alert
, Prompt
도 만들 수 있는데, 이에 대한 코드는 전체 코드에서 볼 수 있다.
감사합니다 사랑합니다