Custom Dialog - (중첩 오버레이에 관하여)

YunShin·2024년 4월 4일

도전과제

목록 보기
3/5

🚩 들어가면서..

dialog ??

dialog(대화 상자) 는 사용자와 컴퓨터 프로그램 간에 정보를 교환하거나 사용자의 입력을 받기 위해 사용되는 ui 요소입니다. modal 이나 popup 을 포함하는 넓은 개념으로 생각해도 좋을 것 같아요.! 👍

dialog 는 다음과 같이 다양한 상황에서 사용할 수 있습니다.

  • 사용자에게 입력 혹은 선택을 요청할 때
  • 경고 메시지를 표시하거나 잘못된 작업을 방지하기 위해 확인 메시지를 제공할 때

프로젝트가 요구하는 기능, 환경에 따라 다양하게 방법으로 dialog 를 구현할 수 있을 것 같아요,,,

브라우저에 직접 alert() , confirm(), prompt() 명령하는 것도 한 방법이겠죠.?

✋ react 를 통해 alert() 과 비슷한 기능을 하는 코드를 구현해 본 적이 있어요.!!
다음 게시물도 봐주시면 좋을 것 같습니다.!
편리한 window.alert(), 직접 만들어 보기

dialog 를 출력하는 건 딱히 어려운 일이 아니었지만,
특정 dialog 에서 결정된 내용을 바탕으로 또 다른 dialog 를 출력한 일은 구현이 조금 힘들다고 생각했어요.

구체적인 예로, 회원가입 에 대한 view 를 구성한다고 생각해봅시다.

- 사용자로부터 인적사항을 입력받을 수 있는 form 을 첫번째 dialog 로 출력
- '이메일' 을 작성하지 않은 상태에서 '중복검사' 버튼을 클릭 시, 경고문을 출력하는 두번째 dialog 를 출력
- '이메일' 을 작성한 상태에서 '중복검사' 버튼을 클릭 시, 중복된 여부를 알리는 세번째 dialog 를 출력
.
.
.

설계에 따라서 더 깊고 복잡한 구조로 dialog 가 중첩돼야 할 수도 있을 겁니다.

특히 위의 예시에서 가장 중요한 점은, 첫번째 dialog 에서 작성한 내용이 그 이후에 출력하는 dialog 의 open/close 상태와 상관없이 원래의 내용을 유지해야한다는 것입니다.

이메일, 비밀번호, 비밀번호 확인 까지 작성한 후에 중복검사를 진행했는데, 기존 정보가 휘발되면 안되니까요.. 😓

이번 포스터에서는 위의 예와 비슷한 요구사항을 해결하기 위해, 일전에 제가 사용했던 코드와 문제점을 소개하고, 이를 더 개선할 수 있는 아이디어에 대해 이야기하겠습니다.!


🫢 내 코드였던 것..

요구사항

전에 FE 담당으로 개발에 참여했던 중고거래 서비스 에서는 ui 설계 때부터 overlay component 를 참 많이 배치했었습니다.

* PEA project - figma design 중 일부

[모달 창]
modal


[채팅 관련]
chatting


[회원가입 form 관련]

경고/알림 메시지 등을 띄우는 것부터 판매자와 구매자 간 채팅기록 또한 dialog를 활용했습니다...

위에서 설명드렸던 것처럼 특정 dialog 위, 사용자의 선택에 따라 또다른 dialog 를 출력해야하는 경우 역시 해당 프로젝트 설계 상에 있었습니다.

제가 담당했던 일 중 하나는, 다양한 overlay-component 를 쉽게 on/off 할 수 있는 기능을 설계하고 그 방법을 팀원들에게 공유하는 것이었습니다.

🧐 설계

우선 overlay-component 의 공통적인 요소 (뒷 배경을 어둡게 하는 필터 등) 를 모아 OverlayBase 컴포넌트를 만들었어요.

const OverlayBase = ({
	position = "midCenter", // 오버레이 컴포넌트 위치
	isFiltered = false, // 뒷 배경을 어둡게 할 필터를 적용할지 여부
	children,
}) => {
	return (
		<>
			{isFiltered && <S.BackgroundFilter />}
			<S.OverlayContainer $positionCSS={position}>
				{children}
			</S.OverlayContainer>
		</>
	);
};

const OverlayContainer = styled.div`
	position: fixed;
	top: 0;
	left: 0;
	z-index: 10000;
	width: 100%;
	height: 100vh;
	padding: 1rem;
	display: flex;
	${({ $positionCSS }) => {
		return PositionCSS[$positionCSS];
	}}
`;
const BackgroundFilter = styled.div`
	position: fixed;
	top: 0;
	left: 0;
	z-index: 9999;
	width: 100%;
	height: 100vh;
	background-color: ${COLOR.COMMON[0]};
	opacity: 0.6;
`;

const S = {
 OverlayContainer,
 BackgroundFilter
}

이를 open/close 할 수 있는 함수를 선언, context-api 를 통해 전역적으로 사용할 수 있도록 했습니다.

저를 포함해 총 일곱 명이 참여했던 만큼, dialog 를 띄우는 로직을 컴포넌트 개별로 작성한다면 핵심적인 view 에 대한 집중이 분산될 거라고 판단했어요.! 🙃

import { createContext, useRef, useState } from "react";
import { OverlayBase } from "../components/overlay";

// overlay-component open 함수를 프로젝트 전반에 전달
export const OverlayContext = createContext({
	onOpenOverlay: () => {},
});

export const OverlayProvider = ({ children }) => {
	/** 오버레이 컴포넌트 */
	const OverlayComponent = useRef(null);
	/** OverlayBase 에 전달될 props  */
	const overlayBaseProps = useRef(null);
	/** overlay 컴포넌트 에 전달할 props */
	const [overlayComponentsProps, setOverlayComponentsProps] = useState({});
	
  	// open 한다는 것은,,
	const onOpenOverlay = ({
		overlayComponent,
		position,
		isFiltered,
		...props
	}) => {
      	// 1. overlay 하려는 컴포넌트를 useRef 에 등록
		OverlayComponent.current = overlayComponent; 
      	overlayBaseProps.current = { position, isFiltered };
		if (
			OverlayComponent.current &&
			typeof OverlayComponent.current === "function"
		) {
          	// 2. overlay 하려는 컴포넌트의 props 를 state 로 등록
			setOverlayComponentsProps(() => ({ ...props }));
		}
	};

	// close 한다는 것은 => 초기화
	const onClose = () => {
		OverlayComponent.current = null;
		overlayBaseProps.current = null;
		setOverlayComponentsProps(() => ({}));
	};

	return (
		<OverlayContext.Provider value={{ onOpenOverlay }}>
			{children}
			
			// useRef 객체에 등록되어있는 컴포넌트가 있다면,
			{OverlayComponent.current && (
              	// OverlayBase + 등록된 컴포넌트 출력
				<OverlayBase {...overlayBaseProps.current}>
					<OverlayComponent.current
						{...overlayComponentsProps}
						onClose={onClose}
					/>
				</OverlayBase>
			)}
		</OverlayContext.Provider>
	);
};

OverlayBase 위에 chatting 혹은 회원가입 form, 단순 알림 텍스트 등 출력해야 할 컴포넌트를 overlay-content 라 하고,

open 하는 함수( = onOpenOverlay ) 의 parameter 로 overlay-content 컴포넌트 를 render 해주는 로직을 구현했습니다.

물론 해당 overlay-content 가 필요로 하는 props 또한 onOpenOverlay 의 parameter 로 받아 대신 전달해 줍니다.

사용 예시

(*다른 팀원 분이 사용해주신,, 실제 코드입니다.)


const SigninPage = () => {
	...
	const { onOpenOverlay } = useOverlay();
	...
	const handleOpenModal = () => {
		onOpenOverlay({
			overlayComponent: Modal, // `Modal` 이라는 컴포넌트를 띄우려나 봅니다.
			noticeText: noticeText, // `Modal 컴포넌트` 로 전달되어야 할 props1
          	modalState: modalState, // `Modal 컴포넌트` 로 전달되어야 할 props2
			buttonText: "확인", // `Modal 컴포넌트` 로 전달되어야 할 props3
			isFiltered: true, 
		});
	};
  	...

저 스스로 이정도(?) 설계를 생각해냈다는 것에 뿌듯하기도 하고, 팀원들한테 칭찬 받을 생각에 으쓱해하며 발표했던 기억이 나네요.. 😎

😱 사건 발단

그 당신엔 뭐 그럭저럭 괜찮다고 생각던 것 같습니다.. (그랬""다구요,,,)

하지만, 너무나 중대한 문제를 간과했습니다.

dialog 가 2 개 이상 동시에 띄어져야 할 경우, 즉 overlay 가 층층이 쌓여야 할 경우에는 props 전달을 어떻게 하죠?

overlay 된 component 에서 다시 onOpenOverlay() 를 호출하면 될까요?

하지만 이것은 틀렸습니다 ~! 🤓

OverlayProvider 를 다시 보면, useRef 객체에 등록하여 관리할 수 있는 component 는 단 한 개 입니다. (useRef 객체를 하나만 선언했습니다..)

설계를 완전히 갈아엎진 않고서는 중첩된 dialog 를 띄울 방법이 전혀 없었어요..

하지만 이것도, 틀렸습니다 ~! 🤓

그럼에도 저는 방법을 꾸역꾸역 찾아냈죠. (근데 이제 광기를 조금 곁들인...)

해결(?) 방안

provider

중첩이 이뤄질 수 없는 이유는 useRef 의 선언이 단 한 개밖에 없기 때문이고, 설령 그것을 여러개 둔다 한들 어느 시점에 어떤 component 를 render 시켜야 할지 분기 처리도 어렵습니다...

그렇다면, component 하나에 고유한 useRef 객체를 하나씩 매핑하는 방법을 찾으면 되지 않을까요?

overlay 컴포넌트를 관리하는 useRef 객체는 OverlayProvider 에 하나씩 선언되어 있으니까,,, OverlayProviderOverlayBase 안에서 재사용하는 방법은 어떤가요?

import { OverlayProvider } from "../../contexts";

const OverlayBase = ({
	position = "midCenter",
	isFiltered = false,
	children,
}) => {
	return (
      
        {/** 
          * OverlayProvider 로 감쌌습니다. 👇 
          * 이 내부에도 useRef, `onOpenOverlay()` 등이 선언되어 있습니다.
          * 현재 컴포넌트(= OverlayBase) 위에서 랜더링 된 컴포넌트 가
          * `onOpenOverlay()` 호출 한다면, 이곳의 useRef 에 등록이 됩니다.
         */}
		<OverlayProvider>
			{isFiltered && <S.BackgroundFilter />}
			<S.OverlayContainer $positionCSS={position}>
				{children}
			</S.OverlayContainer>
		</OverlayProvider>
	);
};

overlay 된 1번 컴포넌트에서 중첩될 2번 컴포넌트 를 대상으로onOpenDialog() 를 호출할 경우, 가까운 provider 의 onOpenDialog() 가 호출된 것이고 그것의 useRef 로 등록이 될 테니 (scope-chaining) , 이론상 무한하게 중첩할 수 있는 구조입니다.!

* (나름 설명하려고 열심히 준비해 봤는데 설명이 구구절절, 더 어려워지는 것 같아요 🥲, 이 부분은 그냥 넘어가셔도 좋습니다... 그냥 넘어가주세요..)

z-index

저는 dialog가 층층이 쌓여야 할 때, 그 순서대로 z-index 값을 증가시켜야 한다고 생각했습니다.

그래서 onOpenDialog() 에 z-index 를 값을 설정할 수 있도록 하고, 팀원들에게 해당 파라미터에 값을 꼭 명시하도록 강제했습니다;;

const OverlayBase = ({
	position = "midCenter",
	isFiltered = false,
	zIndex = 0, // 추가, overlay-component 간에 상대적인 차이
	onClose,
	children,
}) => {
	if (typeof zIndex !== "number" || zIndex < 0) zIndex = 0;
  	...
  	return (
      <OverlayProvider>
      		{isFiltered && <S.BackgroundFilter $zIndex={zIndex} />}
            <S.OverlayContainer
            	$positionCSS={position}
                $zIndex={zIndex}
			...
			</S.OverlayContainer>
		</OverlayProvider>
	);
};


const OverlayContainer = styled.div`
	...
	z-index: ${({ $zIndex }) => 10000 + $zIndex * 2};
    ...
`
const BackgroundFilter = styled.div`
	...
	/** OverlayContainer 보다 한 층 아래 */
	z-index: ${({ $zIndex }) => 10000 + $zIndex * 2 - 1};
	...
`
const S = {
	OverlayContainer,
	BackgroundFilter,
};

"첫번째로 overlay 될 dialog 에는 zIndex 값을 0 으로 주세요. 🙏, 두번째로 overlay 되는 건 z-index 를 1 로 주셔요.🙏 , 근데 중첩 될 필요가 없다고 판단되면 값을 안주셔도 됩니다.🙏, 기본값을 0 으로 설정했거든요.."

중첩되는 dialog 간의 상대적인 위치 값이기 때문에, 꽤 큰 값으로 후처리하는 섬세함(?) 까지 코드에 녹였습니다.

문제점

1. 과부하

일단 OverlayProvider 의 사용 위치가 이상합니다... (내가 쓴 코드지만 😝!)

useContext 는 전역적으로 사용되는 값을 쉽게 전달하도록 함이 그 목적이라고 생각하는데, 위와 같이 일반 컴포넌트에 포함시켜둔다면 render 상태에 따라 그 역할에 제한이 생깁니다.

또한 context-api 에서 관리하는 상태가 변경 될 시, 그 하위의 모든 ui 가 새롭게 그려져 사용을 조심해야하는 것으로 알고 있어요. 지속적으로 provider 객체를 만들어 내는 건 좋지 않을 것 같습니다. 😔

2. 사용하기 어렵다.,

솔직히 저 스스로도 치밀하게 계산해서 작성한 코드라기 보다, 되지 않을까 해서 해봤는데 원하는 대로 화면이 그려진 케이스입니다. 다른 팀원분들에게도 동작 원리가 제대로 공유되었다고 생각되지 않습니다;;

z-index 설정을 필수로 적어달라고 요청한 것 또한 좋지 못한 방법입니다. overlay 누적값을 개발자들에게 계속 계산하도록 시킨 것이니까요...
뭔가 문제가 발생할 경우, 그에 대한 추적을 힘들어지게 될 것도 자명합니다.


✨ 개선된 코드

나름 머리써서 열심히 해봤는데, 누가 봐도 요상한 코드를 적게 되어 실망이 컸습니다. 심지어 프로젝트까지 잘 마무리되지 못해, 조금 더 마음이 아프네요..

그래도 제 dialog 설계를 보시고 스스승님께서 저를 딱하게 여기시어, 한가지 좋은 아이디어💡 를 주셨으니,, 그 뒤로 조금은 더 괜찮은 방법을 생각할 수 있게 되었습니다.

이후부터는 그 방법에 대해 공유해보도록 하겠습니다.!
좋은 방법인지 같이 생각해보고, 다른 방법이 있다면 댓글로 공유해주세요. 👍

main-idea

층층이 dialog 를 쌓아야하는 경우, z-index 를 각각 설정해주어야 한다는 게 저의 기본 전제였습니다.

하지만 이것은 틀렸습니다 ~! 🤓

다음의 코드를 생각해봅시다.

const SomeCompo = () => {
  return (
    <S.Center>
      
			{/** 1. 큰 빨간 네모 */}
      <S.FixedZIndex7 $size="big" $bgColor="red" />
      
			{/** 2. 중간 노란 네모 */}
      <S.FixedZIndex7 $size="middle" $bgColor="yellow" />
      
			{/** 3.. 작은 초록 네모 */}
      <S.FixedZIndex7 $size="small" $bgColor="green" />
      
	</S.Center>
  );
};

const FixedZIndex7 = styled.div`
	position: fixed;
	z-index: 7;
    ...
`
const S = {
	FixedZIndex7
}

display: fixed & z-index: 7, 동일한 css 설정을 갖는 3 개의 컴포넌트에 대해 react 는 어떤 순서로 그려줄까요??

.
.
.
.
.
.
.
.
.
.
.

정답은 3번,

🙄 가장 나중에 쓰인 컴포넌트를 가장 앞에 서도록 그려주네요..

javascript 코드가 위에서부터 한줄씩 읽어지니까 당연하다면 당연한 결과일지도 모르겠어요. 😅

그래도 z-index 값을 계속 증가시키며 설정해줘야 한다고 생각했던 저에겐 나름 쇼킹한 현상이었습니다...

이를 활용하면, 중첩되는 dialog 구현을 조금 더 편하게 할 수 있을 거라고 생각되지 않으신가요??
이를 통해, 저는 아래의 DialogProvider 를 생각해어 다른 프로젝트에 바~로 적용시켜 버렸습니다.! 😊

redesign

context-api 를 활용해, dialog 의 open/close 여부 및 그것이 필요로 하는 props 를 state 로 관리하는 것은 동일합니다. 다만 이번에 달라질 것은,,

1. 여러 dialog 의 props 를 배열로 관리한다.
2. open/close 함수의 역할은 props 배열을 FILO (first-in-last-out) 순으로 관리하는 것으로 정의한다.

바로 코드를 보겠습니다.

// dialog.context.js

// dialog on/off 함수를 전역적으로 전달.!
const DialogContextInit = {
	onOpen: () => {},
	onClose: () => {},
};

const DialogContext = createContext(DialogContextInit);
export const useDialogStore = () => useContext(DialogContext);

export const DialogProvider = ({ children }) => {
  // 여러 dialog 에 전달할 props 객체'들'을 배열 형태로 state 관리.!
	const [dialogAttributesArray, setDialogAttributesArray] = useState([]);
	
  // onOpen 을 호출한다는 것은 👇
	const onOpen = ({ ...props }) => {
		setDialogAttributesArray((prev) => {
			const _prev = [...prev];
			props.isOpen = true;
          	// 새로 출력할 dialog 의 props 를
          	// 배열의 가장 마지막 요소로 추가하는 것.!
			_prev.push(props);
			return _prev;
		});
	};
	
  // onClose 을 호출한다는 것은 👇
	const onClose = () => {
		setDialogAttributesArray((prev) => {
			if (!!!prev.length) return prev;
			const _prev = [...prev];
          	// props 배열의 가장 마지막 요소를 제거하는 것.!
			const latestDialogsProps = _prev.pop();
			latestDialogsProps.isOpen = false;
			return _prev;
		});
	};

	return (
		<DialogContext.Provider value={{ ...{ onOpen, onClose } }}>
			{children}
			// push 된 순서대로 `Dialog` render
			{dialogAttributesArray.map((attributes, idx) => (
				<Dialog key={idx} {...attributes} />
			))}
		</DialogContext.Provider>
	);
};

주석으로 간간히 작성했지만, 사실 주석 내용이 전부 입니다. 😅

Dialog 컴포넌트 를 어떻게 설계하는 지에 따라 관리하는 props 객체가 달라지겠지만,,

open 된 순서로 ( = push() 된 순서로) Dialog 컴포넌트 에 전달 -> render 시키는 과정입니다.

🫠 이해를 돕기 위해, 한 가지 케이스에 대해 나름대로 쉽게 설명해보겠습니다.

프로젝트 전역에서 onOpen() 이 3 번 호출되었고, 해당 함수를 통해 전달된 dialog props 가 다음과 같은 형태로 state 에 등록되었다고 가정합니다.

[
	{title:'dialog1', message:"1번, 나 열림"},
    {title:'dialog2', message:"2번, 나도 열림"},
    {title:'dialog3', message:"3번, 나는 왜?"},
]

현재 state 가 빈 배열이 아니니, Dialog 컴포넌트 가 배열의 수만큼 render 됩니다.

return (
		<DialogContext.Provider value={{ ...{ onOpen, onClose } }}>
			{children}
			{dialogAttributesArray.map((attributes, idx) => (
				<Dialog key={idx} {...attributes} />
			))}
		</DialogContext.Provider>
	);

조금 더 풀어서 보면,,

return (
		<DialogContext.Provider value={{ ...{ onOpen, onClose } }}>
			{children}
            <Dialog title="dialog1" message="1번, 나 열림" />
            <Dialog title="dialog2" message="2번, 나도 열림" />
            <Dialog title="dialog3" message="3번, 나는 왜?" />
        </DialogContext.Provider>
	);

main-idea section 에서 보았듯이, 가장 마지막에 open(= push) 된 props 의 Dialog 가 가장 상단이 노출됩니다..

📺 사용 예 & 실행화면

실제 사용을 해보겠습니다.

DialogContext, DialogProvider 는 위에 코드로 작성해두었으니, 또 작성하진 않겠습니다.! 🙏

저는 Dialog 컴포넌트를 아래와 같이 작성하여, DialogProvider 에 심었습다.

이건 그냥 참고만 해주시길 바랍니다.!

export type DialogProps = {
	isOpen?: boolean // 현재 열린 상태인지,
	size?: keyof typeof SIZE_CSS 
	onClose?: VoidFunction // 최상단의 `Dialog 컴포넌트` 제거
	title: string
	isCloseButton?: boolean
	dialogContent?: ReactNode // Dialog 위에 그려질 컴포넌트
}
const Dialog = ({
	size = 'default',
	onClose,
	title,
	isCloseButton = true,
	dialogContent
}: T.DialogProps) => {
	return (
		<S.FullSizeFilter>
			<CenterFlexBox align="bothAlign">
				<S.DialogBase $size={size} onClick={(e) => e.stopPropagation()}>
					{isCloseButton && <S.CloseButton onClick={onClose}>X</S.CloseButton>}
					<ColumnFlexBox gap="1rem">
						<S.DialogTitle>{title}</S.DialogTitle>
						<CenterFlexBox align="bothAlign">{dialogContent}</CenterFlexBox>
					</ColumnFlexBox>
				</S.DialogBase>
			</CenterFlexBox>
		</S.FullSizeFilter>
	)
}
export default Dialog

이제 Provider 를 가장 상위 파일 (저같은 경우엔 src > main.tsx) 에 감싸주시고, 그곳의 호출을 받는 하위 컴포넌트에서 Dialog 를 열어봅시다.

// src > App.jsx
const App = () => {
	const { onOpen, onClose } = useDialogStore()
	const onClickButton = () => {
		onOpen({
			title: '⚠️ Alert',
			size: 'small',
			dialogContent: <h2>😁</h2>, // dialog 위에 출력될 컴포넌트
			onClose
		})
	}
	return (
		<S.MainContainer>
			<Button onClick={onClickButton}>그냥 버튼</Button>
		</S.MainContainer>
	)
}

dialog

잘 작동합니다. 👍👍

이제 오늘의 목적이었던, 중첩이 가능한지를 확인해보겠습니다.
간단하게 설계를 해보자면,,

1. 현재 페이지에서 버튼을 누르면, 두 개의 버튼을 포함하는 컴포넌트를 dialog 위에 출력된다.
2. 이 중, 어떤 버튼을 누르냐에 따라 각각 다른 텍스트를 가진 dialog 가 중첩된다.

첫번째로 overlay 될 컴포넌트를 다음과 같이 작성했습니다.

const HasTwoButton = () => {
	const { onOpen, onClose } = useDialogStore()
	/** 1번 버튼 클릭 시, */
	const onClickButton1 = () =>
		onOpen({
			title: '1번 버튼 타이틀',
			size: 'small',
			dialogContent: <p>1번 버튼 내용</p>,
			onClose
		})
	/** 2번 버튼 클릭 시, */
	const onClickButton2 = () =>
		onOpen({
			title: '2번 버튼 타이틀',
			size: 'small',
			dialogContent: <p>2번 버튼 내용</p>,
			onClose
		})
	return (
		<S.MainContainer>
			<Button onClick={onClickButton1}>1번 버튼</Button>
			<Button onClick={onClickButton2}>2번 버튼</Button>
		</S.MainContainer>
	)
}

설계에 따라 두 개의 버튼을 포함하는 view 를 가지고, 각 버튼 클릭 시에 등장할 두 번째 overlay 컴포넌트 도 지정해 두었습니다.

이제 첫 페이지를 다음과 같이 수정하면,

const App = () => {
	const { onOpen, onClose } = useDialogStore()
	const onClickButton = () => {
		onOpen({
			title: '⚠️ Alert',
			dialogContent: <HasTwoButton />, // 첫번째 overlay component 지정
			onClose
		})
	}
	return (
		<S.MainContainer>
			<Button onClick={onClickButton}>그냥 버튼</Button>
		</S.MainContainer>
	)
}

자주 사용하는 dialog-props 설정 등을 모아 custom-hook 으로 만들어 두면, 보다 직관적으로 분기처리할 수 있을 것 같습니다. 🫠


🚁 또 다른 방법

<dialog> - html element

HTML5 에 dialog 태그가 있다는 것을 알고 계신가요??
2022년부터 Chorem, Firefox, Safari 등 주요 browser 에서 지원하는 엄연한 html element 라고 합니다.


(*2024.04.05 기준)
사용 가능한 browswer/version 확인하기

저도 이걸 사용하는 코드를 봤는데, 제가 개선한 코드보다 훨씬 사용하기 편한 것 같더라구요... 😭 (겹쳐지는 것도 가능..)

그래도,, 혹시라도 위의 사진에서 지원 안되는 빨간색이 신경 쓰인다면,, 혹은 레트로 감성을 좋아하신다면,, 혹은 map 으로 순회출력(?) 하는 게 좋으시다면,, 제 코드(?)도 참고해 주시길 바랍니다~ 🙄

참고하기
따끈따끈한 HTML5의 <dialog> 요소를 지금 사용해보세요.

profile
😎👍

0개의 댓글