REACT ๋ชจ๋ฌ, ํ ์คํธ, ๋๋ก์ด ๋ฑ ํ์ ์ปดํฌ๋ํธ ์ค๊ณ ๋ฐ ๊ตฌํ
์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ํ์ ์์๋ ์ฌ์ฉ์์์ ์ํธ์์ฉ์ ๊ฐ์ ํ๊ณ ์ค์ํ ์ ๋ณด๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ ๋ฌํ๋ ๋ฐ ํต์ฌ์ ์ธ ์ญํ ์ ํฉ๋๋ค. ๋ชจ๋ฌ, ํ ์คํธ, ๋๋ก์ด ๋ฑ์ ํ์ ์ปดํฌ๋ํธ๋ ํ๋ฉด ๋ด์์ ์ฃผ์๋ฅผ ์ง์ค์ํค๊ฑฐ๋ ๋ถ๊ฐ์ ์ธ ๊ธฐ๋ฅ์ ์ ๊ณตํ ๋ ๋ง์ด ํ์ฉ๋ฉ๋๋ค.
์ด๋ฒ ํฌ์คํ ์์๋ ํ์ ์ปดํฌ๋ํธ์ ๋์์ธ ์์น๊ณผ ๊ตฌํ ๋ฐฉ๋ฒ, ๊ทธ๋ฆฌ๊ณ ์ ๋๋ฉ์ด์ ๋ฐ ์ ๊ทผ์ฑ ๊ณ ๋ ค์ฌํญ์ ๋ํด ์์ธํ ์ค๋ช ํฉ๋๋ค.
ํ์ ์ปดํฌ๋ํธ์ ๊ธฐ๋ณธ ๊ฐ๋ ๊ณผ ์ค์์ฑ
ํ์ ์ปดํฌ๋ํธ๋ ์ฌ์ฉ์ ์ธํฐํ์ด์ค(UI)์์ ๋ณ๋์ ์ค๋ฒ๋ ์ด ์ฐฝ์ผ๋ก ๋ํ๋, ์ค์ํ ๋ฉ์์ง, ํผ๋๋ฐฑ, ์ต์ ๋๋ ์ถ๊ฐ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ์์์ ๋๋ค.
- ๋ชจ๋ฌ(Modal): ์ฌ์ฉ์๊ฐ ๋ชจ๋ฌ ์ฐฝ์ ๋ซ๊ธฐ ์ ๊น์ง ๋ค๋ฅธ ์์ ์ ์ํํ์ง ๋ชปํ๋๋ก ํ๋ ํ์ ์์๋ก, ์ค์ํ ๊ฒฝ๊ณ ๋ฉ์์ง๋ ํผ ์ ๋ ฅ ๋ฑ์ ์ฃผ๋ก ์ฌ์ฉ๋ฉ๋๋ค.
- ํ ์คํธ(Toast): ํ๋ฉด ํ๋จ์ด๋ ์๋จ์ ์ ์ ํ์๋์ด ๊ฐ๋จํ ์๋ฆผ์ด๋ ํผ๋๋ฐฑ์ ์ ๊ณตํ๋ ์์๋ก, ์๋์ผ๋ก ์ฌ๋ผ์ง๋ฉฐ ์ฌ์ฉ์์ ํ๋ฆ์ ๋ฐฉํดํ์ง ์์ต๋๋ค.
- ๋๋ก์ด(Drawer): ํ๋ฉด ์ธก๋ฉด์์ ์ฌ๋ผ์ด๋ ํํ๋ก ๋ํ๋๋ ๋ฉ๋ด ๋๋ ํจ๋๋ก, ์ถ๊ฐ ์ต์ ์ด๋ ๋ด๋น๊ฒ์ด์ ์ ์ ๊ณตํ๋ ๋ฐ ์ ์ฉํฉ๋๋ค.
์ด๋ค ํ์ ์ปดํฌ๋ํธ๋ ์ ์ ํ ์ ๋๋ฉ์ด์ ํจ๊ณผ์ ์ ๊ทผ์ฑ ๊ธฐ๋ฅ์ด ๊ฒฐํฉ๋ ๊ฒฝ์ฐ, ์ฌ์ฉ์ ๊ฒฝํ(UX)์ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ต๋๋ค.
๋์์ธ ์์น๊ณผ UI/UX ๊ณ ๋ ค์ฌํญ
ํ์ ์ปดํฌ๋ํธ๋ฅผ ์ค๊ณํ ๋๋ ๋ช ๊ฐ์ง ํต์ฌ ์์น์ ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
1. ์๊ฐ์ ๊ณ์ธต ๊ตฌ์กฐ์ ์ฃผ์ ์ง์ค
- ์ค๋ฒ๋ ์ด ํจ๊ณผ: ๋ชจ๋ฌ ์ฐฝ์ด๋ ๋๋ก์ด๋ ๋ฐฐ๊ฒฝ์ ์ด๋ก๊ฒ ์ฒ๋ฆฌํ์ฌ ์ฌ์ฉ์์ ์์ ์ ์์ฐ์ค๋ฝ๊ฒ ํ์ ์์๋ก ์ง์ค์ํต๋๋ค.
- ๋ช ํํ ๊ฒฝ๊ณ: ํ์ ์ปดํฌ๋ํธ๋ ์ฃผ๋ณ ์ฝํ ์ธ ์ ํ์ฐํ ๊ตฌ๋ถ๋๋ ํ ๋๋ฆฌ๋ ๊ทธ๋ฆผ์ ํจ๊ณผ๋ฅผ ์ฃผ์ด, ์๊ฐ์ ๋ถ๋ฆฌ๊ฐ์ ์ ๊ณตํฉ๋๋ค.
2. ์ ๋๋ฉ์ด์ ๊ณผ ์ ํ ํจ๊ณผ
์ ๋๋ฉ์ด์ ์ ํ์ ์์๊ฐ ๋ํ๋๊ณ ์ฌ๋ผ์ง ๋ ์ฌ์ฉ์์๊ฒ ๋ถ๋๋ฌ์ด ์ ํ ํจ๊ณผ๋ฅผ ์ ๊ณตํ์ฌ, ๊ฐ์์ค๋ฌ์ด ํ๋ฉด ์ ํ์ผ๋ก ์ธํ ํผ๋์ ์ค์ ๋๋ค.
- ํ์ด๋ ์ธ/์์ ํจ๊ณผ: ๋ชจ๋ฌ ์ฐฝ์ด๋ ํ ์คํธ๊ฐ ์์ํ ๋ํ๋๊ณ ์ฌ๋ผ์ง๋ ํจ๊ณผ๋ฅผ ์ ์ฉํ๋ฉด, ์๊ฐ์ ์์ฐ์ค๋ฌ์์ด ์ฆ๋๋ฉ๋๋ค.
- ์ฌ๋ผ์ด๋ ํจ๊ณผ: ๋๋ก์ด์ ๊ฒฝ์ฐ, ์ข์ธก ๋๋ ์ฐ์ธก์์ ๋ถ๋๋ฝ๊ฒ ์ฌ๋ผ์ด๋๋์ด ๋ฑ์ฅํ๋ฉด ์ฌ์ฉ์๊ฐ ์ง๊ด์ ์ผ๋ก ๋ฉ๋ด์ ํ๋ฆ์ ์ดํดํ ์ ์์ต๋๋ค.
3. ์ ๊ทผ์ฑ ๊ณ ๋ ค์ฌํญ
ํ์ ์ปดํฌ๋ํธ๋ ๋ชจ๋ ์ฌ์ฉ์์๊ฒ ์ ๊ทผ ๊ฐ๋ฅํด์ผ ํฉ๋๋ค. ์ ๊ทผ์ฑ์ ๋์ด๊ธฐ ์ํ ๋ช ๊ฐ์ง ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ํค๋ณด๋ ๋ด๋น๊ฒ์ด์ : ๋ชจ๋ฌ ์ฐฝ์์๋ ํญ ํค๋ฅผ ์ด์ฉํด ํ์ ๋ด๋ถ ์์ ๊ฐ ์ด๋์ด ๊ฐ๋ฅํ๋๋ก ํ๊ณ , ESC ํค๋ฅผ ๋๋ฅด๋ฉด ์ฐฝ์ด ๋ซํ๋๋ก ๊ตฌํํฉ๋๋ค.
- ARIA ์์ฑ: aria-modal, aria-labelledby, aria-describedby ๋ฑ์ ์์ฑ์ ์ถ๊ฐํ์ฌ, ์คํฌ๋ฆฐ ๋ฆฌ๋๊ฐ ํ์ ์ ์ญํ ๊ณผ ๋ด์ฉ์ ๋ช ํํ๊ฒ ์ธ์ํ ์ ์๋๋ก ํฉ๋๋ค.
- ํฌ์ปค์ค ๊ด๋ฆฌ: ํ์ ์ด ์ด๋ฆด ๋ ํฌ์ปค์ค๋ฅผ ํด๋น ์ปดํฌ๋ํธ ๋ด์ ์ฒซ ๋ฒ์งธ ํฌ์ปค์ค ๊ฐ๋ฅํ ์์๋ก ์ด๋์ํค๊ณ , ๋ซํ ๋ ์ด์ ํฌ์ปค์ค ์์น๋ก ๋ณต์ํ๋ ๋ฐฉ์์ผ๋ก ์ฌ์ฉ์ ํผ๋์ ์ต์ํํฉ๋๋ค.
๋ชจ๋ฌ ์ปดํฌ๋ํธ ๊ตฌํ ๋ฐฉ๋ฒ
๋ชจ๋ฌ์ ์ค์ํ ์ ๋ณด๋ฅผ ์ฌ์ฉ์์๊ฒ ์ ๋ฌํ๊ฑฐ๋, ์ค์ํ ์ ๋ ฅ์ ์๊ตฌํ ๋ ์ฌ์ฉ๋ฉ๋๋ค. ๋ฆฌ์กํธ์์ ๋ชจ๋ฌ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ๋ ๊ธฐ๋ณธ์ ์ธ ๋ฐฉ๋ฒ์ ์ํ ๊ด๋ฆฌ๋ฅผ ํตํด ๋ชจ๋ฌ์ ์ด๋ฆผ/๋ซํ์ ์ ์ดํ๊ณ , ์ค๋ฒ๋ ์ด์ ๋ด๋ถ ์ฝํ ์ธ ๋ฅผ ๋ณ๋์ ์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌํ๋ ๊ฒ์ ๋๋ค.
๋ชจ๋ฌ ์ปดํฌ๋ํธ ์ฝ๋ ์์
import React, { useEffect, useRef } from 'react';
import styled from 'styled-components';
const Overlay = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.3s ease;
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`;
const ModalContent = styled.div`
background: #fff;
padding: 20px;
width: 90%;
max-width: 500px;
border-radius: 8px;
position: relative;
animation: slideIn 0.3s ease;
@keyframes slideIn {
from { transform: translateY(-20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
`;
const CloseButton = styled.button`
position: absolute;
top: 10px;
right: 10px;
border: none;
background: transparent;
font-size: 1.2rem;
cursor: pointer;
`;
const Modal = ({ isOpen, onClose, children, titleId, descriptionId }) => {
const modalRef = useRef(null);
useEffect(() => {
if (isOpen && modalRef.current) {
modalRef.current.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<Overlay onClick={onClose}>
<ModalContent
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={descriptionId}
tabIndex={-1}
ref={modalRef}
onClick={e => e.stopPropagation()}
>
<CloseButton onClick={onClose} aria-label="๋ซ๊ธฐ ๋ฒํผ">×</CloseButton>
{children}
</ModalContent>
</Overlay>
);
};
export default Modal;
์ ์ฝ๋๋ ๋ชจ๋ฌ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ ์์ ์ ๋๋ค.
- ์ค๋ฒ๋ ์ด: ์ ์ฒด ํ๋ฉด์ ๋ฎ๋ ๋ฐํฌ๋ช ๋ฐฐ๊ฒฝ์ ์ ๊ณตํ์ฌ ๋ชจ๋ฌ์ ์ง์คํ ์ ์๊ฒ ํฉ๋๋ค.
- ์ ๋๋ฉ์ด์ : fadeIn๊ณผ slideIn ์ ๋๋ฉ์ด์ ์ ์ ์ฉํ์ฌ ๋ชจ๋ฌ์ด ๋ถ๋๋ฝ๊ฒ ๋ํ๋๋๋ก ํฉ๋๋ค.
- ์ ๊ทผ์ฑ: ์ญํ (role), aria ์์ฑ, ํฌ์ปค์ค ๊ด๋ฆฌ๋ฅผ ํตํด ์คํฌ๋ฆฐ ๋ฆฌ๋ ์ฌ์ฉ์์ ํค๋ณด๋ ์ฌ์ฉ์ ๋ชจ๋์๊ฒ ์ ๊ทผ์ฑ์ ๋ณด์ฅํฉ๋๋ค.
ํ ์คํธ ์ปดํฌ๋ํธ ๊ตฌํ ๋ฐฉ๋ฒ
ํ ์คํธ๋ ๊ฐ๋จํ ์๋ฆผ ๋ฉ์์ง๋ฅผ ์ ๊ณตํ๋ ํ์ ์์๋ก, ์งง์ ์๊ฐ ๋์ ํ๋ฉด์ ํ์๋๋ฉฐ ์๋์ผ๋ก ์ฌ๋ผ์ง๋๋ค. ์ฌ์ฉ์์๊ฒ ์ค์ํ ์ํ ๋ณํ๋ ํผ๋๋ฐฑ์ ์ ๊ณตํ๋ ๋ฐ ํจ๊ณผ์ ์ ๋๋ค.
ํ ์คํธ ์ปดํฌ๋ํธ ์ฝ๋ ์์
import React, { useEffect } from 'react';
import styled from 'styled-components';
const ToastContainer = styled.div`
position: fixed;
bottom: 20px;
right: 20px;
background: #333;
color: #fff;
padding: 15px 20px;
border-radius: 4px;
opacity: 0;
animation: slideUp 0.5s forwards, fadeOut 0.5s forwards 3s;
z-index: 1000;
@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
`;
const Toast = ({ message, onClose }) => {
useEffect(() => {
const timer = setTimeout(() => {
onClose();
}, 3500); // 3.5์ด ํ ํ ์คํธ ์ข
๋ฃ
return () => clearTimeout(timer);
}, [onClose]);
return <ToastContainer>{message}</ToastContainer>;
};
export default Toast;
ํ ์คํธ ์ปดํฌ๋ํธ๋
- ์ ๋๋ฉ์ด์ : slideUp๊ณผ fadeOut ์ ๋๋ฉ์ด์ ์ ํตํด ๋ถ๋๋ฝ๊ฒ ๋ฑ์ฅํ๊ณ ์ฌ๋ผ์ง๋๋ค.
- ์๋ ์ข ๋ฃ: ์ผ์ ์๊ฐ ํ ์๋์ผ๋ก onClose ์ฝ๋ฐฑ์ ํธ์ถํ์ฌ ํ ์คํธ๋ฅผ ์ ๊ฑฐํฉ๋๋ค.
๋๋ก์ด ์ปดํฌ๋ํธ ๊ตฌํ ๋ฐฉ๋ฒ
๋๋ก์ด๋ ํ๋ฉด์ ์ข์ธก์ด๋ ์ฐ์ธก์์ ์ฌ๋ผ์ด๋๋์ด ๋ํ๋๋ ํจ๋๋ก, ๋ด๋น๊ฒ์ด์ ๋ฉ๋ด, ์ถ๊ฐ ์ต์ ๋๋ ํํฐ๋ง ๊ธฐ๋ฅ ๋ฑ์ ์ ๊ณตํ ๋ ์ ์ฉํฉ๋๋ค. ๋๋ก์ด๋ ์ฌ์ฉ์๊ฐ ํ์ํ ์์ ์๋ง ๋ํ๋๋ฏ๋ก ํ๋ฉด ๊ณต๊ฐ์ ํจ์จ์ ์ผ๋ก ํ์ฉํ ์ ์์ต๋๋ค.
๋๋ก์ด ์ปดํฌ๋ํธ ์ฝ๋ ์์
import React from 'react';
import styled from 'styled-components';
const DrawerContainer = styled.div`
position: fixed;
top: 0;
${props => (props.position === 'left' ? 'left: 0;' : 'right: 0;')}
width: 300px;
height: 100%;
background: #fff;
box-shadow: ${props => (props.position === 'left' ? '2px 0 5px rgba(0,0,0,0.3)' : '-2px 0 5px rgba(0,0,0,0.3)')};
transform: translateX(${props => (props.open ? '0' : props.position === 'left' ? '-100%' : '100%')});
transition: transform 0.3s ease;
z-index: 1000;
`;
const DrawerContent = styled.div`
padding: 20px;
`;
const Drawer = ({ open, onClose, position = 'left', children }) => {
return (
<>
{open && (
<div
onClick={onClose}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
zIndex: 900,
}}
/>
)}
<DrawerContainer open={open} position={position}>
<DrawerContent>
{children}
</DrawerContent>
</DrawerContainer>
</>
);
};
export default Drawer;
๋๋ก์ด ์ปดํฌ๋ํธ๋
- ์ฌ๋ผ์ด๋ ์ ๋๋ฉ์ด์ : transform๊ณผ transition์ ์ด์ฉํด ๋ถ๋๋ฝ๊ฒ ์ด๋ฆฌ๊ณ ๋ซํ๋๋ก ํฉ๋๋ค.
- ์ค๋ฒ๋ ์ด: ๋๋ก์ด๊ฐ ์ด๋ ธ์ ๋ ๋ฐฐ๊ฒฝ์ ์ด๋ก๊ฒ ์ฒ๋ฆฌํ์ฌ, ์ฌ์ฉ์๊ฐ ๋๋ก์ด ์ธ๋ถ๋ฅผ ํด๋ฆญํ๋ฉด ๋ซํ๋๋ก ํฉ๋๋ค.
๊ฒฐ๋ก
๋ชจ๋ฌ, ํ ์คํธ, ๋๋ก์ด์ ๊ฐ์ ํ์ ์ปดํฌ๋ํธ๋ ์ฌ์ฉ์์๊ฒ ์ค์ํ ํผ๋๋ฐฑ๊ณผ ๋ถ๊ฐ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ๋์์, ์ ์ฒด UI์ ์ผ๊ด์ฑ๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๋ ๋ฐ ํต์ฌ์ ์ธ ์ญํ ์ ํฉ๋๋ค.
- ๋์์ธ ์ธก๋ฉด์์๋ ์๊ฐ์ ๊ณ์ธต ๊ตฌ์กฐ, ์ ๋๋ฉ์ด์ ์ ํ, ์ ๊ทผ์ฑ ๊ณ ๋ ค์ฌํญ์ ์ค์ ์ ์ผ๋ก ๋ฐ์ํ์ฌ ์ฌ์ฉ์ ์นํ์ ์ธ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌ์ถํด์ผ ํฉ๋๋ค.
- ๊ตฌํ ์ธก๋ฉด์์๋ ๋ฆฌ์กํธ์ ์ํ ๊ด๋ฆฌ์ ์ด๋ฒคํธ ํธ๋ค๋ง์ ํจ๊ณผ์ ์ผ๋ก ํ์ฉํ์ฌ, ๊ฐ ํ์ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ์ ์ดํ ์ ์๋๋ก ๋ชจ๋ํ๋ ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค.
- ์ ๊ทผ์ฑ๊ณผ ์ ๋๋ฉ์ด์ ์ ๊ณ ๋ คํ ์ค๊ณ๋ ์คํฌ๋ฆฐ ๋ฆฌ๋์ ํค๋ณด๋ ๋ด๋น๊ฒ์ด์ ์ ์ง์ํ๋ฉฐ, ์ฌ์ฉ์ ํผ๋๋ฐฑ์ ์์ฐ์ค๋ฝ๊ฒ ์ ๋ํ ์ ์๋ ์ค์ํ ์์์ ๋๋ค.
์ค์ ํ๋ก์ ํธ์์๋ ๊ฐ ํ์ ์ปดํฌ๋ํธ๋ฅผ ๋์์ธ ์์คํ ์ ์ผ๋ถ๋ก ํตํฉํ์ฌ, ํ ๋ด์์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ณ ์ ์ง๋ณด์ํ๊ธฐ ์ฌ์ด ํํ๋ก ๊ด๋ฆฌํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ๋ํ, ์ฌ์ฉ์ ํ ์คํธ๋ฅผ ํตํด ์ ๋๋ฉ์ด์ ์๋, ์ธํฐ๋์ ํ๋ฆ, ์ ๊ทผ์ฑ ๊ธฐ๋ฅ ๋ฑ์ ์ง์์ ์ผ๋ก ๊ฐ์ ํ๋ฉด, ์ ์ฒด์ ์ธ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ํ์ง๊ณผ ์ฌ์ฉ์ ๋ง์กฑ๋๋ฅผ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ต๋๋ค.
ํ์ ์ปดํฌ๋ํธ ์ค๊ณ์ ๊ตฌํ์ ๋จ์ํ UI ์์๋ฅผ ๋์ด์, ์ฌ์ฉ์ ๊ฒฝํ์ ๊ทน๋ํํ๊ณ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๊ณต์ ์ธ ์ด์์ ๊ธฐ์ฌํ๋ ํต์ฌ ์ ๋ต์ ๋๋ค. ์์ผ๋ก ๋ชจ๋ฌ, ํ ์คํธ, ๋๋ก์ด ๋ฑ์ ํ์ ์์๋ฅผ ํ์ฉํ ๋ค์ํ UI/UX ๊ฐ์ ๋ฐฉ๋ฒ์ ์ ์ฉํ์ฌ, ์ฌ์ฉ์๊ฐ ๋ณด๋ค ์ง๊ด์ ์ด๊ณ ์พ์ ํ๊ฒ ์น ์๋น์ค๋ฅผ ์ด์ฉํ ์ ์๋๋ก ์ง์์ ์ผ๋ก ๊ฐ์ ํด ๋๊ฐ์๊ธธ ๋ฐ๋๋๋ค.
๋๊ธ