* 프로그래머스, 타입스크립트로 함께하는 웹 풀 사이클 개발(React, Node.js) 5기 강의 수강 내용을 정리하는 포스팅.
* 원활한 내용 이해를 위해 수업에서 제시된 자료 이외에, 개인적으로 조사한 자료 등을 덧붙이고 있음.
import React, { useState, useRef, useEffect } from 'react';
interface DropdownProps {
options: string[];
onSelect: (option: string) => void;
}
const Dropdown: React.FC<DropdownProps> = ({ options, onSelect }) => {
const [open, setOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setOpen(false);
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div ref={dropdownRef} style={{ position: 'relative', display: 'inline-block' }}>
<button onClick={() => setOpen(!open)}>Toggle Dropdown</button>
{open && (
<ul
style={{
listStyle: 'none',
padding: 0,
margin: 0,
position: 'absolute',
background: '#fff',
boxShadow: '0 2px 5px rgba(0,0,0,0.15)',
}}
>
{options.map((option, index) => (
<li
key={index}
onClick={() => {
onSelect(option);
setOpen(false);
}}
style={{ padding: '8px 12px', cursor: 'pointer' }}
>
{option}
</li>
))}
</ul>
)}
</div>
);
};
export default Dropdown;
import React, { useState } from 'react';
interface Tab {
label: string;
content: React.ReactNode;
}
interface TabsProps {
tabs: Tab[];
}
const Tabs: React.FC<TabsProps> = ({ tabs }) => {
const [activeIndex, setActiveIndex] = useState(0);
return (
<div>
<div style={{ display: 'flex', borderBottom: '1px solid #ccc' }}>
{tabs.map((tab, index) => (
<button
key={index}
onClick={() => setActiveIndex(index)}
style={{
padding: '10px 20px',
border: 'none',
borderBottom: activeIndex === index ? '2px solid blue' : 'none',
background: 'none',
cursor: 'pointer',
}}
>
{tab.label}
</button>
))}
</div>
<div style={{ padding: '20px' }}>{tabs[activeIndex].content}</div>
</div>
);
};
export default Tabs;
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
useEffect(() => {
const handleEsc = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleEsc);
}
return () => {
document.removeEventListener('keydown', handleEsc);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return ReactDOM.createPortal(
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<div
style={{ background: '#fff', padding: '20px', borderRadius: '4px' }}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>,
document.getElementById('modal-root') as HTMLElement
);
};
export default Modal;
import React, { useEffect } from 'react';
interface ToastProps {
message: string;
duration?: number;
onClose: () => void;
}
const Toast: React.FC<ToastProps> = ({ message, duration = 3000, onClose }) => {
useEffect(() => {
const timer = setTimeout(() => {
onClose();
}, duration);
return () => clearTimeout(timer);
}, [duration, onClose]);
return (
<div
style={{
position: 'fixed',
bottom: '20px',
right: '20px',
background: '#333',
color: '#fff',
padding: '10px 20px',
borderRadius: '4px',
boxShadow: '0 2px 5px rgba(0,0,0,0.3)',
}}
>
{message}
</div>
);
};
export default Toast;
확장 아이디어: 여러 토스트 메시지를 관리하기 위해 Toast Manager 컴포넌트를 추가로 구현할 수 있습니다.
loadMore 함수를 호출하여 추가 데이터를 불러옵니다.hasMore) 여부를 체크하여 불필요한 호출을 방지합니다.hasMore 상태가 변경될 때 옵저버를 해제합니다.import React, { useEffect, useRef } from 'react';
interface InfiniteScrollProps {
loadMore: () => Promise<void>;
hasMore: boolean;
children: React.ReactNode;
}
const InfiniteScroll: React.FC<InfiniteScrollProps> = ({ loadMore, hasMore, children }) => {
const loader = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!loader.current || !hasMore) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: 1.0 }
);
observer.observe(loader.current);
return () => {
if (loader.current) {
observer.unobserve(loader.current);
}
};
}, [loader, loadMore, hasMore]);
return (
<div>
{children}
{hasMore && (
<div ref={loader} style={{ height: '100px', textAlign: 'center' }}>
Loading more...
</div>
)}
</div>
);
};
export default InfiniteScroll;
각 컴포넌트는 React의 상태 관리와 이벤트 처리 메커니즘을 활용하여 재사용성과 유지보수성을 높였습니다.
필요에 따라 디자인, 접근성(ARIA) 적용, 성능 최적화, 그리고 에러 핸들링을 추가하면 더욱 견고한 컴포넌트를 만들 수 있습니다.
import React from 'react';
import './ResponsiveComponent.css';
const ResponsiveComponent: React.FC = () => {
return (
<div className="container">
<div className="sidebar">Sidebar Content</div>
<div className="main-content">Main Content</div>
</div>
);
};
export default ResponsiveComponent;
.container {
display: flex;
flex-direction: row;
}
.sidebar {
width: 25%;
background-color: #f2f2f2;
padding: 20px;
}
.main-content {
width: 75%;
padding: 20px;
}
/* 화면 너비가 768px 이하인 경우 레이아웃 변경 */
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.sidebar,
.main-content {
width: 100%;
}
}
react-responsive를 사용하면 React 컴포넌트 내에서 쉽게 조건부 렌더링을 적용할 수 있습니다.
라이브러리 설치
npm install react-responsive
import React from 'react';
import MediaQuery from 'react-responsive';
const ResponsiveMedia: React.FC = () => {
return (
<div>
{/* 데스크탑 및 태블릿 가로 모드 */}
<MediaQuery minWidth={769}>
<div style={{ backgroundColor: '#e0f7fa', padding: '20px' }}>
데스크탑/태블릿 레이아웃
</div>
</MediaQuery>
{/* 모바일 화면 */}
<MediaQuery maxWidth={768}>
<div style={{ backgroundColor: '#ffe0b2', padding: '20px' }}>
모바일 레이아웃
</div>
</MediaQuery>
</div>
);
};
export default ResponsiveMedia;