Custom Dialog - (window.alert() 직접 만들어보기)

YunShin·2024년 3월 23일
2

도전과제

목록 보기
2/5
post-thumbnail

🚩 들어가면서..

브라우저에서 경고/알림 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() 구현에는 이 조건이 반드시 필요합니다.! )


🔥 구현

Component 📦

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,
}

Context 💧

요구사항 ⓪ 과 관련하여, 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')
		})
	}

🎸 참고 및 기타

profile
😎👍

0개의 댓글