src/components/CanvasItem.jsx
import { Link } from 'react-router-dom';
function CanvasItem({ id, title, lastModified, category }) {
return (
<Link
className="bg-white rounded-lg shadow-md overflow-hidden transition-transform duration-300 hover:scale-105"
to={`/canvases/${id}`}
>
<div className="p-6">
<h2 className="text-2xl font-bold mb-2 text-gray-800">{title}</h2>
<p className="text-sm text-gray-600 mb-4">
최근 수정일: {lastModified}
</p>
<span className="inline-block px-3 py-1 text-sm font-semibold text-gray-700 bg-gray-200 rounded-full">
{category}
</span>
</div>
</Link>
);
}
export default CanvasItem;
CanvasItem
컴포넌트는 개별 린캔버스 항목을 카드 형태로 표시합니다.Link
컴포넌트를 사용하여 클릭 시 해당 린캔버스의 상세 페이지로 이동합니다.title
, lastModified
, category
를 받아서 각 항목의 정보를 표시합니다.src/components/CanvasList.jsx
import CanvasItem from './CanvasItem';
function CanvasList({ filteredData, searchText, isGridView }) {
if (filteredData.length === 0) {
return (
<div className="text-center py-10">
<p className="text-xl text-gray-600">
{searchText ? '검색 결과가 없습니다' : '목록이 없습니다'}
</p>
</div>
);
}
return (
<div
className={`grid gap-6 ${isGridView ? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3' : 'grid-cols-1'}`}
>
{filteredData.map(item => (
<CanvasItem
key={item.id}
id={item.id}
title={item.title}
lastModified={item.lastModified}
category={item.category}
/>
))}
</div>
);
}
export default CanvasList;
CanvasList
컴포넌트는 필터링된 린캔버스 데이터를 목록 또는 그리드 형태로 표시합니다.filteredData
가 비어있을 경우, 적절한 메시지를 중앙에 표시합니다.CanvasItem
컴포넌트를 사용하여 각 항목을 렌더링합니다.isGridView
상태에 따라 그리드 컬럼 수를 조절하여 레이아웃을 변경합니다.src/components/SearchBar.jsx
import { FaSearch } from 'react-icons/fa';
function SearchBar({ searchText, setSearchText }) {
return (
<div className="relative w-full sm:w-64 mb-4 sm:mb-0">
<input
type="text"
placeholder="검색"
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
value={searchText}
onChange={e => setSearchText(e.target.value)}
aria-label="검색"
/>
<FaSearch className="absolute left-3 top-3 text-gray-400" />
</div>
);
}
export default SearchBar;
SearchBar
컴포넌트는 사용자가 린캔버스를 검색할 수 있는 입력 필드를 제공합니다.input
필드를 통해 검색어를 입력받고, searchText
상태를 업데이트합니다.FaSearch
아이콘을 입력 필드 내부에 위치시켜 시각적인 검색 UI를 강화합니다.aria-label
을 통해 스크린 리더 사용자에게 입력 필드의 용도를 명확히 전달합니다.src/components/ViewToggle.jsx
import { FaTh, FaList } from 'react-icons/fa';
function ViewToggle({ isGridView, setIsGridView }) {
return (
<div className="flex space-x-2">
<button
onClick={() => setIsGridView(true)}
className={`p-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${isGridView ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
aria-label="Grid view"
>
<FaTh />
</button>
<button
onClick={() => setIsGridView(false)}
className={`p-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${!isGridView ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
aria-label="List view"
>
<FaList />
</button>
</div>
);
}
export default ViewToggle;
ViewToggle
컴포넌트는 사용자에게 그리드 보기와 리스트 보기 간의 전환 옵션을 제공합니다.FaTh
아이콘과 FaList
아이콘)을 통해 사용자가 원하는 보기 방식을 선택할 수 있습니다.isGridView
상태에 따라 활성화된 버튼에 다른 스타일을 적용하여 현재 선택된 뷰를 시각적으로 표시합니다.aria-label
을 통해 버튼의 목적을 명확히 전달합니다.src/pages/Home.jsx
import { useState } from 'react';
import CanvasList from '../components/CanvasList';
import SearchBar from '../components/SearchBar';
import ViewToggle from '../components/ViewToggle';
function Home() {
const [searchText, setSearchText] = useState('');
const [isGridView, setIsGridView] = useState(true);
const dummyData = [
{
id: 1,
title: '친환경 도시 농업 플랫폼',
lastModified: '2023-06-15',
category: '농업',
},
{
id: 2,
title: 'AI 기반 건강 관리 앱',
lastModified: '2023-06-10',
category: '헬스케어',
},
{
id: 3,
title: '온디맨드 물류 서비스',
lastModified: '2023-06-05',
category: '물류',
},
{
id: 4,
title: 'VR 가상 여행 서비스',
lastModified: '2023-06-01',
category: '여행',
},
];
const filteredData = dummyData.filter(item =>
item.title.toLowerCase().includes(searchText.toLowerCase()),
);
return (
<div className="container mx-auto px-4 py-16">
<div className="mb-6 flex flex-col sm:flex-row items-center justify-between">
<SearchBar searchText={searchText} setSearchText={setSearchText} />
<ViewToggle isGridView={isGridView} setIsGridView={setIsGridView} />
</div>
<CanvasList
filteredData={filteredData}
isGridView={isGridView}
searchText={searchText}
/>
</div>
);
}
export default Home;
Home
페이지는 검색 기능과 뷰 토글 기능을 갖춘 린캔버스 목록을 표시합니다.useState
훅을 사용하여 searchText
와 isGridView
상태를 관리합니다.dummyData
배열을 통해 예시 데이터를 정의하고, 이를 기반으로 필터링된 데이터를 생성합니다.SearchBar
컴포넌트를 사용하여 사용자의 검색 입력을 받습니다.ViewToggle
컴포넌트를 통해 그리드 보기와 리스트 보기 간의 전환을 제공합니다.CanvasList
컴포넌트를 사용하여 필터링된 린캔버스 데이터를 목록 형태로 렌더링합니다.searchText
와 isGridView
상태를 상위 컴포넌트에서 관리하여 하위 컴포넌트에 전달함으로써 데이터 흐름을 명확히 했습니다.CanvasItem
: 개별 린캔버스 항목을 카드 형태로 표시.CanvasList
: 필터링된 데이터를 목록 또는 그리드 형태로 렌더링.SearchBar
: 검색 입력 필드를 제공.ViewToggle
: 그리드 보기와 리스트 보기 간의 전환 기능 제공.Home
페이지에서 searchText
와 isGridView
상태를 관리하고, 이를 하위 컴포넌트에 전달하여 검색과 뷰 전환 기능을 구현합니다.sm:flex-row
를 통해 작은 화면에서는 세로로, 큰 화면에서는 가로로 배치됩니다.aria-label
속성을 사용하여 버튼과 입력 필드의 목적을 명확히 전달했습니다. 이는 스크린 리더 사용자에게 유용합니다.transition-transform
, duration-300
, hover:scale-105
등을 활용하여 사용자 인터랙션 시 부드러운 애니메이션 효과를 추가했습니다.react-icons
라이브러리를 사용하여 시각적인 아이콘을 추가함으로써 UI의 직관성과 미적 요소를 강화했습니다.이번 실습에서는 UI 컴포넌트를 효과적으로 분리하여 린캔버스 목록 페이지를 구현했습니다. 각 컴포넌트는 특정 역할에 집중하여 코드의 재사용성과 유지보수성을 높였으며, 반응형 디자인과 접근성을 고려하여 모든 사용자가 편리하게 이용할 수 있도록 했습니다. Tailwind CSS와 React Icons를 활용하여 깔끔하고 직관적인 UI를 구현함으로써 사용자 경험을 향상시켰습니다.
src/components/CanvasItem.jsx
import { Link } from 'react-router-dom';
import { FaTrash } from 'react-icons/fa'; // 삭제 아이콘 추가
function CanvasItem({ id, title, lastModified, category, onDelete }) { // onDelete prop 추가
return (
<Link
className="relative bg-white rounded-lg shadow-md overflow-hidden transition-transform duration-300 hover:scale-105" // relative 클래스 추가
to={`/canvases/${id}`}
>
<div className="p-6">
<h2 className="text-2xl font-bold mb-2 text-gray-800">{title}</h2>
<p className="text-sm text-gray-600 mb-4">
최근 수정일: {lastModified}
</p>
<span className="inline-block px-3 py-1 text-sm font-semibold text-gray-700 bg-gray-200 rounded-full">
{category}
</span>
</div>
<button
className="absolute top-2 right-2 p-2 text-red-500 rounded-full" // 삭제 버튼 스타일링
aria-label="Delete"
onClick={onDelete} // 클릭 시 onDelete 핸들러 호출
>
<FaTrash />
</button>
</Link>
);
}
export default CanvasItem;
react-icons
라이브러리에서 FaTrash
아이콘을 임포트하여 삭제 버튼에 사용합니다. 이는 사용자가 직관적으로 삭제 기능을 인식할 수 있게 도와줍니다.CanvasItem
컴포넌트에 onDelete
prop을 추가하여 부모 컴포넌트로부터 삭제 핸들러를 전달받습니다. 이를 통해 각 항목별로 삭제 동작을 처리할 수 있습니다.button
요소를 추가하여 삭제 기능을 구현했습니다. 버튼은 absolute
포지셔닝을 사용하여 카드의 오른쪽 상단에 배치됩니다.onClick
이벤트 핸들러로 onDelete
함수를 호출하여 삭제 동작을 수행합니다.relative
) 추가:Link
컴포넌트에 relative
클래스를 추가하여 삭제 버튼이 카드 내에서 정확하게 위치할 수 있도록 합니다.src/components/CanvasList.jsx
import CanvasItem from './CanvasItem';
function CanvasList({ filteredData, searchText, isGridView, onDeleteItem }) { // onDeleteItem prop 추가
if (filteredData.length === 0) {
return (
<div className="text-center py-10">
<p className="text-xl text-gray-600">
{searchText ? '검색 결과가 없습니다' : '목록이 없습니다'}
</p>
</div>
);
}
return (
<div
className={`grid gap-6 ${isGridView ? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3' : 'grid-cols-1'}`}
>
{filteredData.map(item => (
<CanvasItem
key={item.id}
id={item.id}
title={item.title}
lastModified={item.lastModified}
category={item.category}
onDelete={e => { // onDelete prop 전달
e.preventDefault(); // Link의 기본 동작 방지
onDeleteItem(item.id); // 부모의 삭제 핸들러 호출
}}
/>
))}
</div>
);
}
export default CanvasList;
CanvasList
컴포넌트에 onDeleteItem
prop을 추가하여 부모 컴포넌트로부터 삭제 핸들러를 전달받습니다.CanvasItem
컴포넌트에 onDelete
prop을 전달합니다. 이는 삭제 버튼 클릭 시 부모의 삭제 핸들러가 호출되도록 합니다.onDelete
함수에서는 e.preventDefault()
를 호출하여 Link
의 기본 동작(페이지 이동)을 방지하고, onDeleteItem
을 통해 해당 항목의 id
를 전달합니다.onDelete
핸들러가 실행되어 해당 항목의 id
를 부모 컴포넌트에 전달합니다. 부모 컴포넌트는 이를 통해 상태를 업데이트하여 항목을 목록에서 제거합니다.src/pages/Home.jsx
import { useState } from 'react';
import CanvasList from '../components/CanvasList';
import SearchBar from '../components/SearchBar';
import ViewToggle from '../components/ViewToggle';
function Home() {
const [searchText, setSearchText] = useState('');
const [isGridView, setIsGridView] = useState(true);
const [dummyData, setDummyData] = useState([
{
id: 1,
title: '친환경 도시 농업 플랫폼',
lastModified: '2023-06-15',
category: '농업',
},
{
id: 2,
title: 'AI 기반 건강 관리 앱',
lastModified: '2023-06-10',
category: '헬스케어',
},
{
id: 3,
title: '온디맨드 물류 서비스',
lastModified: '2023-06-05',
category: '물류',
},
{
id: 4,
title: 'VR 가상 여행 서비스',
lastModified: '2023-06-01',
category: '여행',
},
]);
const handleDeleteItem = id => {
setDummyData(dummyData.filter(item => item.id !== id));
};
const filteredData = dummyData.filter(item =>
item.title.toLowerCase().includes(searchText.toLowerCase()),
);
return (
<div className="container mx-auto px-4 py-16">
<div className="mb-6 flex flex-col sm:flex-row items-center justify-between">
<SearchBar searchText={searchText} setSearchText={setSearchText} />
<ViewToggle isGridView={isGridView} setIsGridView={setIsGridView} />
</div>
<CanvasList
filteredData={filteredData}
isGridView={isGridView}
searchText={searchText}
onDeleteItem={handleDeleteItem} // 삭제 핸들러 전달
/>
</div>
);
}
export default Home;
useState
를 사용하여 dummyData
상태를 관리합니다. 초기값으로 린캔버스 항목들을 정의합니다.handleDeleteItem
함수는 특정 id
를 가진 항목을 dummyData
에서 제거하는 역할을 합니다.setDummyData
를 사용하여 dummyData
상태를 업데이트합니다. filter
메소드를 통해 삭제할 항목을 제외한 나머지 항목들로 새로운 배열을 생성합니다.CanvasList
컴포넌트에 onDeleteItem
prop을 전달하여 삭제 동작을 하위 컴포넌트로 전달합니다.CanvasItem
에서 삭제 버튼을 클릭할 때 handleDeleteItem
함수가 호출되어 해당 항목이 삭제됩니다.searchText
: 사용자가 입력한 검색어를 관리하여 리스트를 필터링합니다.isGridView
: 그리드 보기와 리스트 보기 상태를 관리하여 레이아웃을 전환합니다.dummyData
: 린캔버스 목록 데이터를 관리하여 UI에 표시합니다.컴포넌트 분리:
CanvasItem
: 개별 린캔버스 항목을 표시하며, 삭제 버튼을 포함합니다.CanvasList
: 필터링된 데이터를 목록 또는 그리드 형태로 렌더링하며, 삭제 기능을 지원합니다.SearchBar
: 검색 입력 필드를 제공하여 사용자가 린캔버스를 검색할 수 있게 합니다.ViewToggle
: 그리드 보기와 리스트 보기 간의 전환을 제공합니다.삭제 기능 구현:
CanvasItem
에 삭제 버튼을 추가하여 사용자가 특정 린캔버스를 삭제할 수 있게 했습니다.CanvasList
는 삭제 이벤트를 처리하기 위해 onDeleteItem
함수를 받아 CanvasItem
에 전달합니다.Home
페이지에서 dummyData
상태를 관리하며, 삭제 이벤트 시 해당 항목을 dummyData
에서 제거합니다.상태 관리:
Home
페이지에서 주요 상태(searchText
, isGridView
, dummyData
)를 관리하여 자식 컴포넌트로 전달합니다.반응형 디자인:
sm:flex-row
와 같은 클래스는 작은 화면에서는 세로로, 큰 화면에서는 가로로 레이아웃을 변경합니다.grid-cols-1 sm:grid-cols-2 lg:grid-cols-3
클래스를 사용하여 다양한 화면 크기에 맞춰 유연하게 레이아웃을 조정합니다.접근성:
aria-label
속성을 사용하여 버튼의 목적을 명확히 전달했습니다. 이는 스크린 리더 사용자에게 유용합니다.이번 실습에서는 삭제 버튼 UI를 구현하여 린캔버스 목록에서 개별 항목을 삭제할 수 있는 기능을 추가했습니다. 컴포넌트를 분리하여 코드의 재사용성과 유지보수성을 높였으며, React 상태 관리를 통해 동적인 UI 업데이트를 가능하게 했습니다. Tailwind CSS와 React Icons를 활용하여 깔끔하고 직관적인 사용자 인터페이스를 구현했습니다.
삭제 기능을 추가함으로써 사용자에게 더 나은 경험을 제공하고, 애플리케이션의 기능성을 향상시킬 수 있었습니다.