
브라우저에서 경고/알림 dialog 를 생성하는 window.alert(), 다들 한번쯤 사용해 보셨을 겁니다.!
window.alert()
사용자에게 전달한 텍스트와확인버튼이 있는 대화창(dialog)을 생성할 수 있습니다.// 사용 예시 const onClickButton = () => alert('버튼을 누르셨네요? 흠.. 🤔')
alert() 과 같이 java-script 에서 기본으로 제공하는 (최소한의) 사용자 인터페이스, confirm(), prompt() 도 있어요!!
window.confirm()
취소버튼이 추가되었습니다.// 사용 예시 const onClickButton = () => confirm('취소 버튼 누를려고요?? 흠.. 🤔')
window.prompt()
입력창까지 제공합니다. 👍// 사용 예시 const onClickButton = () => prompt('잘 적어야겠죠?? 🤭')
간단한 한 줄이면 코드 가독성을 해치지 않으면서, 적절한 상황에 필요한 메시지를 사용자에게 전달할 수 있어 아~주 편리한 메소드라고 생각해요.
다만, 사용하는 브라우저마다 제공되는 인터페이스가 다르고, 프로젝트의 디자인과 통일성이 없어 완성도를 떨어뜨릴 수 있어요.!
물론 css 를 조작하여 디자인을 변경할 수도 있지만, 최소한의 기능만 제공하는 인터페이스에 의존하는 것보다 서비스가 요구하는 기능을 가진 dialog 를 직접 설계하는 훨씬 좋지 않을까요!? 👍
이 아래부터는 alert(), confirm(), propmt() 중, 가~장 쉬운 기능을 가진 alert 메서드를 직접 구현하는 과정입니다.🫠 천천히 이해해보면, confirm(), propmt() 도 분명 구현할 수 있을 거예요.!!
다음의 목표를 갖고, window.alert() 을 대체할 dialog 를 띄우는 로직을 구현해봅시다.
⓪. 전역적으로 사용할 수 있어야 합니다.!
①. 코드 가독성을 해치지 않아야 합니다.
쉽게 호출하여, 각 상황에 어떤 텍스트가 보여질 지 바로 이해할 수 있어야 합니다.
②. 다음과 같이, dialog 가 열려야 할 페이지 컴포넌트마다 상태를 정의하여 open/close 하는 코드는 지양합니다.!
import { useState } from 'react'; import { Alert } from '@/components'; const SomePage = () => { const [isAlertOpen, setIsAlertOpen] = useState(false); . . . return ( <> {isAlertOpen && <Alert text="alert 입니다."/>} {/* SomePage 에서 나타나야 할 ui */} </> ) }
SomePage의 view 에 대한 집중도를 낮출 뿐만 아니라,, 한 페이지 컴포넌트 안에서 alert 이 열려야 할 상황이 많아질 경우, 그에 대한 분기처리까지 진행해야 합니다. 👎
③. '확인' 버튼 클릭 이후이 로직 또한 개발자에 의해 정의될 수 있어야 합니다.
alert 메소드의 경우 '확인' 버튼 클릭 시, 해당 dialog 가 닫히는 것으로 끝나지만, 서비스 설계에 따라 그 이후의 로직까지 수행할 수 있도록 기능을 확장해보겠습니다. ( confirm(), propmt() 구현에는 이 조건이 반드시 필요합니다.! )
alert() 구현을 위해 다음과 같이 dialog 에 전달할 최소한의 타입을 선언하겠습니다.
type DialogProps = {
isOpen: boolean
text: string
onConfirm: VoidFunction
}
이를 활용해, Dialog 컴포넌트를 간단하게 생성했습니다.
import styled from 'styled-components'
const Dialog = (props: DialogProps) => {
return (
<S.Wrapper>
<h1>경고창</h1>
<p>{props.text}</p>
<S.Button onClick={props.onConfirm}>확인</S.Button>
</S.Wrapper>
)
}
export default Dialog
const Wrapper = styled.div`
position: fixed;
z-index: 5;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
min-width: 15rem;
min-height: 20rem;
width: fit-content;
height: fit-content;
border: 1px solid #888;
`
const Button = styled.div`
width: fit-content;
height: fit-content;
padding: 2rem;
`
const S = {
Wrapper,
Button,
}
요구사항 ⓪ 과 관련하여, dialog 가 현재 열렸는지 닫혔는지, 열려 있다면 어떤 텍스트가 보여지는 지 등을 공유하기 위해서 context-api 를 활용하겠습니다.
// 전역적으로 전달될 값은 dialog 를 열고 닫을 수 있는 함수입니다.
// window.alert 역시 함수를 호출하는 것이죠:)
export const DialogContext = createContext<DiaContextProps>({
onOpenDialog: () => {},
onCloseDialog: () => {},
})
이제 DialogContext 와 관련된 Provider 를 정의하고, 최상위 .tsx 파일에서 import 하겠습니다.
// Dialog 컴포넌트의 props의 초기 상태를 정의했습니다.
const initDialogAttribute: DialogProps = {
isOpen: false,
text: '',
onConfirm: () => {},
}
export const DialogProvider = ({children}: PropsWithChildren) => {
const [dialogAttribute, setDialogAttribute] =
useState<DialogProps>(initDialogAttribute)
/**
* DialogProps 중, text 혹은 onConfirm 값이 입력된다면 이를 Dialog 컴포넌트로 전달합니다.
*/
const onOpenDialog = (args: Partial<DialogProps>) => {
setDialogAttribute((prev) => ({
...prev,
...args,
isOpen: true,
}))
}
/**
* 다시 초기 상태로 되돌렸습니다.
* initDialogAttribute.isClose 는 false 이므로 dialog 가 닫히게 됩니다.
*/
const onCloseDialog = () => {
setDialogAttribute(initDialogAttribute)
}
return (
<DialogContext.Provider value={{onOpenDialog, onCloseDialog}}>
{children}
/**
* dialogAttribute.isOpen 시에만, Dialog 가 render 됩니다.
* z-index 값을 설정했으므로, 대부분의 {children} 보다 앞서 나타납니다.
*/
{dialogAttribute.isOpen && <Dialog {...dialogAttribute} />}
</DialogContext.Provider>
)
}
이제 src > main.tsx 에 DialogProvider 를 추가하겠습니다.
// main.tsx
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<DialogProvider>
<RouterProvider router={router} />
</DialogProvider>
</React.StrictMode>,
)
import {DialogContext} from '@/contexts/dialog-context'
import {useContext} from 'react'
import styled from 'styled-components'
const SomePage = () => {
const {onOpenDialog, onCloseDialog} = useContext(DialogContext)
const navigate = useNavigate()
const onClickButton1 = () => {
onOpenDialog({
text: '🤖: 이해가 됐습니까?, Human',
onConfirm: onCloseDialog,
})
}
const onClickButton2 = () => {
onOpenDialog({
text: '🤖: 그럼 이동합니다. Human',
onConfirm: () => {
navigate('/to')
onCloseDialog()
},
})
}
return (
<S.Wrapper>
⛳️ 여기가 SomePage
<button onClick={onClickButton1}>확인 누르면 그냥 꺼지는 dialog</button>
<button onClick={onClickButton2}>
2번 dialog 는 확인 누르면 페이지 이동을 한다.
</button>
</S.Wrapper>
)
}
export default SomePage
const Wrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
width: 100%;
height: 100vh;
`
const S = {
Wrapper,
}
🎥 시연영상
대부분의 요구사항이 제대로 충족된 거 같습니다만, 약간 아쉽네요.😅
window.alert() 와 비교해봅시다.!
// 1
const onClickButton = () => {
alert('🤖: 이해가 됐습니까?, Human')
}
// 2
const {onOpenDialog, onCloseDialog} = useContext(DialogContext)
const onClickButton = () => {
onOpenDialog({
text: '🤖: 이해가 됐습니까?, Human',
onConfirm: onCloseDialog,
})
}
최종적으로 만들고 싶었던 형태에 아직 못 미친 것 같아요., 더 간단하게 쓸 수 있도록 반복되는 부분을 제외하는 등 조치 해보겠습니다.
1.
확인버튼 클릭 이후에, 추가 로직이 있든 없든onCloseDialog()는 무조건 실행해야합니다.
2. (Optional) 객체 형태로 파라미터를 전달하지 말고, 문자열만 전달해도 될 것 같아요. (파라미터가 많은 것도 아니니,,) 그 편이 alert 과 형태가 비슷해질 것도 같습니다.
위 사항을 고려하여, DialogContext 를 활용한 custom-hook 을 만들어 보겠습니다.
//use-alert.ts
export const useAlert = () => {
const {onOpenDialog, onCloseDialog} = useContext(DialogContext)
const onOpenAlert = (text: string, callbackFunc?: VoidFunction) => {
onOpenDialog({
text,
onConfirm: () => {
callbackFunc && callbackFunc()
onCloseDialog()
},
})
}
return {onOpenAlert}
}
바~로 비교 해봅시다.
// 1
const onClickButton = () => {
alert('🤖: 이해가 됐습니까?, Human')
}
// 2
const {onOpenAlert} = useAlert()
const onClickButton = () => {
onOpenAlert('🤖: 이해가 됐습니까?, Human')
}
훅함수 호출부만 제외하면, 애초 목표를 제대로 달성한 것 같습니다.
버튼 클릭 시 추가로 수행해야할 로직이 있더라도 아래와 같이 전달할 경우, 정상 작동합니다. 🙃
const navigate = useNavigate()
const {onOpenAlert} = useAlert()
const onClickButton = () => {
onOpenAlert('🤖: 이동합니다.', () => {
navigate('/to')
})
}