React - UI 컴포넌트 분리, 삭제 버튼 UI

김명원·2025년 1월 8일
0

learnReact

목록 보기
14/26

UI 컴포넌트 분리 실습 설명

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 컴포넌트를 사용하여 클릭 시 해당 린캔버스의 상세 페이지로 이동합니다.
    • Tailwind CSS 클래스를 활용하여 카드의 배경색, 모서리 둥글기, 그림자, 호버 시 확대 효과 등을 적용했습니다.
    • title, lastModified, category를 받아서 각 항목의 정보를 표시합니다.
  • 장점:
    • 재사용 가능: 여러 린캔버스 항목을 동일한 스타일로 쉽게 렌더링할 수 있습니다.
    • 유지보수 용이: UI 변경 시 이 컴포넌트만 수정하면 전체 항목에 적용됩니다.

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를 강화합니다.
    • Tailwind CSS 클래스를 사용하여 입력 필드의 스타일과 포지셔닝을 설정했습니다.
  • 장점:
    • 접근성 고려: 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 상태에 따라 활성화된 버튼에 다른 스타일을 적용하여 현재 선택된 뷰를 시각적으로 표시합니다.
    • Tailwind CSS 클래스를 사용하여 버튼의 스타일과 반응형 디자인을 구현했습니다.
  • 장점:
    • 사용자 경험 향상: 간단한 UI 요소로 보기 방식을 쉽게 전환할 수 있습니다.
    • 재사용 가능: 여러 목록 페이지에서 동일한 뷰 토글 기능을 제공할 수 있습니다.
    • 접근성 고려: 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 훅을 사용하여 searchTextisGridView 상태를 관리합니다.
    • dummyData 배열을 통해 예시 데이터를 정의하고, 이를 기반으로 필터링된 데이터를 생성합니다.
    • SearchBar 컴포넌트를 사용하여 사용자의 검색 입력을 받습니다.
    • ViewToggle 컴포넌트를 통해 그리드 보기와 리스트 보기 간의 전환을 제공합니다.
    • CanvasList 컴포넌트를 사용하여 필터링된 린캔버스 데이터를 목록 형태로 렌더링합니다.
  • 장점:
    • 컴포넌트 분리: 각 기능별로 컴포넌트를 분리하여 코드의 재사용성과 유지보수성을 높였습니다.
    • 상태 관리: searchTextisGridView 상태를 상위 컴포넌트에서 관리하여 하위 컴포넌트에 전달함으로써 데이터 흐름을 명확히 했습니다.
    • 반응형 디자인: Tailwind CSS를 활용하여 다양한 화면 크기에 대응하는 레이아웃을 구현했습니다.
    • 사용자 경험 향상: 검색 기능과 보기 토글 기능을 통해 사용자가 원하는 방식으로 린캔버스를 탐색할 수 있도록 했습니다.

전체적인 구조와 동작 방식

  • 컴포넌트 분리: UI를 여러 작은 컴포넌트로 분리하여 각 컴포넌트가 하나의 역할에 집중하도록 했습니다. 이는 코드의 가독성과 재사용성을 높입니다.
    • CanvasItem: 개별 린캔버스 항목을 카드 형태로 표시.
    • CanvasList: 필터링된 데이터를 목록 또는 그리드 형태로 렌더링.
    • SearchBar: 검색 입력 필드를 제공.
    • ViewToggle: 그리드 보기와 리스트 보기 간의 전환 기능 제공.
  • 상태 관리:
    • Home 페이지에서 searchTextisGridView 상태를 관리하고, 이를 하위 컴포넌트에 전달하여 검색과 뷰 전환 기능을 구현합니다.
  • 반응형 디자인:
    • Tailwind CSS의 유틸리티 클래스를 사용하여 다양한 화면 크기에 적응하는 레이아웃을 구현했습니다. 예를 들어, sm:flex-row를 통해 작은 화면에서는 세로로, 큰 화면에서는 가로로 배치됩니다.
  • 접근성:
    • aria-label 속성을 사용하여 버튼과 입력 필드의 목적을 명확히 전달했습니다. 이는 스크린 리더 사용자에게 유용합니다.
  • 애니메이션과 트랜지션:
    • Tailwind CSS의 transition-transform, duration-300, hover:scale-105 등을 활용하여 사용자 인터랙션 시 부드러운 애니메이션 효과를 추가했습니다.
  • 아이콘 사용:
    • react-icons 라이브러리를 사용하여 시각적인 아이콘을 추가함으로써 UI의 직관성과 미적 요소를 강화했습니다.

결론

이번 실습에서는 UI 컴포넌트를 효과적으로 분리하여 린캔버스 목록 페이지를 구현했습니다. 각 컴포넌트는 특정 역할에 집중하여 코드의 재사용성과 유지보수성을 높였으며, 반응형 디자인과 접근성을 고려하여 모든 사용자가 편리하게 이용할 수 있도록 했습니다. Tailwind CSS와 React Icons를 활용하여 깔끔하고 직관적인 UI를 구현함으로써 사용자 경험을 향상시켰습니다.


삭제 버튼 UI

실습

1. 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;

설명:

  • FaTrash 아이콘 추가:
    • react-icons 라이브러리에서 FaTrash 아이콘을 임포트하여 삭제 버튼에 사용합니다. 이는 사용자가 직관적으로 삭제 기능을 인식할 수 있게 도와줍니다.
  • onDelete Prop 추가:
    • CanvasItem 컴포넌트에 onDelete prop을 추가하여 부모 컴포넌트로부터 삭제 핸들러를 전달받습니다. 이를 통해 각 항목별로 삭제 동작을 처리할 수 있습니다.
  • 삭제 버튼 추가:
    • button 요소를 추가하여 삭제 기능을 구현했습니다. 버튼은 absolute 포지셔닝을 사용하여 카드의 오른쪽 상단에 배치됩니다.
    • onClick 이벤트 핸들러로 onDelete 함수를 호출하여 삭제 동작을 수행합니다.
    • Tailwind CSS 클래스를 사용하여 버튼의 위치, 크기, 색상, 라운드 처리 등을 스타일링했습니다.
  • 상대 포지셔닝(relative) 추가:
    • Link 컴포넌트에 relative 클래스를 추가하여 삭제 버튼이 카드 내에서 정확하게 위치할 수 있도록 합니다.

2. 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;

설명:

  • onDeleteItem Prop 추가:
    • CanvasList 컴포넌트에 onDeleteItem prop을 추가하여 부모 컴포넌트로부터 삭제 핸들러를 전달받습니다.
  • CanvasItem에 onDelete 전달:
    • CanvasItem 컴포넌트에 onDelete prop을 전달합니다. 이는 삭제 버튼 클릭 시 부모의 삭제 핸들러가 호출되도록 합니다.
    • onDelete 함수에서는 e.preventDefault()를 호출하여 Link의 기본 동작(페이지 이동)을 방지하고, onDeleteItem을 통해 해당 항목의 id를 전달합니다.
  • 삭제 동작:
    • 사용자가 삭제 버튼을 클릭하면 onDelete 핸들러가 실행되어 해당 항목의 id를 부모 컴포넌트에 전달합니다. 부모 컴포넌트는 이를 통해 상태를 업데이트하여 항목을 목록에서 제거합니다.

3. 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;

설명:

  • dummyData 상태 추가:
    • useState를 사용하여 dummyData 상태를 관리합니다. 초기값으로 린캔버스 항목들을 정의합니다.
    • 이는 실제 데이터베이스나 API에서 데이터를 가져오는 대신 예시 데이터를 사용하여 UI를 테스트하기 위함입니다.
  • handleDeleteItem 함수:
    • handleDeleteItem 함수는 특정 id를 가진 항목을 dummyData에서 제거하는 역할을 합니다.
    • setDummyData를 사용하여 dummyData 상태를 업데이트합니다. filter 메소드를 통해 삭제할 항목을 제외한 나머지 항목들로 새로운 배열을 생성합니다.
  • CanvasList에 onDeleteItem 전달:
    • CanvasList 컴포넌트에 onDeleteItem prop을 전달하여 삭제 동작을 하위 컴포넌트로 전달합니다.
    • 이를 통해 CanvasItem에서 삭제 버튼을 클릭할 때 handleDeleteItem 함수가 호출되어 해당 항목이 삭제됩니다.
  • 상태 관리:
    • searchText: 사용자가 입력한 검색어를 관리하여 리스트를 필터링합니다.
    • isGridView: 그리드 보기와 리스트 보기 상태를 관리하여 레이아웃을 전환합니다.
    • dummyData: 린캔버스 목록 데이터를 관리하여 UI에 표시합니다.

전체적인 구조와 동작 방식

  • 컴포넌트 분리:

    • UI를 여러 개의 작은 컴포넌트로 분리하여 각 컴포넌트가 하나의 역할을 담당하도록 설계했습니다.
      • CanvasItem: 개별 린캔버스 항목을 표시하며, 삭제 버튼을 포함합니다.
      • CanvasList: 필터링된 데이터를 목록 또는 그리드 형태로 렌더링하며, 삭제 기능을 지원합니다.
      • SearchBar: 검색 입력 필드를 제공하여 사용자가 린캔버스를 검색할 수 있게 합니다.
      • ViewToggle: 그리드 보기와 리스트 보기 간의 전환을 제공합니다.
  • 삭제 기능 구현:

    • CanvasItem에 삭제 버튼을 추가하여 사용자가 특정 린캔버스를 삭제할 수 있게 했습니다.
    • CanvasList는 삭제 이벤트를 처리하기 위해 onDeleteItem 함수를 받아 CanvasItem에 전달합니다.
    • Home 페이지에서 dummyData 상태를 관리하며, 삭제 이벤트 시 해당 항목을 dummyData에서 제거합니다.
  • 상태 관리:

    • Home 페이지에서 주요 상태(searchText, isGridView, dummyData)를 관리하여 자식 컴포넌트로 전달합니다.
    • 상태가 변경되면 React는 자동으로 UI를 업데이트하여 반영합니다.
  • 반응형 디자인:

    • Tailwind CSS를 사용하여 반응형 레이아웃을 구현했습니다. 예를 들어, sm:flex-row와 같은 클래스는 작은 화면에서는 세로로, 큰 화면에서는 가로로 레이아웃을 변경합니다.
    • 그리드와 리스트 보기는 Tailwind CSS의 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 클래스를 사용하여 다양한 화면 크기에 맞춰 유연하게 레이아웃을 조정합니다.
  • 접근성:

    • aria-label 속성을 사용하여 버튼의 목적을 명확히 전달했습니다. 이는 스크린 리더 사용자에게 유용합니다.
    • 시맨틱 HTML 요소와 적절한 ARIA 속성을 사용하여 접근성을 향상시켰습니다.

결론

이번 실습에서는 삭제 버튼 UI를 구현하여 린캔버스 목록에서 개별 항목을 삭제할 수 있는 기능을 추가했습니다. 컴포넌트를 분리하여 코드의 재사용성과 유지보수성을 높였으며, React 상태 관리를 통해 동적인 UI 업데이트를 가능하게 했습니다. Tailwind CSS와 React Icons를 활용하여 깔끔하고 직관적인 사용자 인터페이스를 구현했습니다.

삭제 기능을 추가함으로써 사용자에게 더 나은 경험을 제공하고, 애플리케이션의 기능성을 향상시킬 수 있었습니다.


profile
개발자가 되고 싶은 정치학도생의 기술 블로그

0개의 댓글