프론트엔드

[React] 린캔버스 프로젝트

밍들레밍 2025. 4. 6. 22:42

섹션 14. 웹 프로젝트 시작하기

-린캔버스(Lean Canvas)란?

  • 스타트업과 같은 신생 기업이 비즈니스 모델을 빠르게 정리하고 검증할 수 있도록 도와주는 비즈니스 모델 프레임워크
  • 린캔버스는 복잡한 사업 계획을 간단하게 정리할 수 있는 1페이지 짜리 도구로, 9개의 핵심 요소로 구성됨
    1. 문제(Problem): 고객이 겪는 주요 문제들을 명확하게 정의
    2. 고객 세그먼트(Customer Segments): 타겟 고객층을 구체적으로 식별
    3. 독특한 가치 제안(Unique Value Proposition): 고객에게 제공할 핵심 가치
    4. 해결책(Solution): 문제를 해결할 수 있는 구체적인 솔루션
    5. 채널(Channels): 고객에게 도달할 수 있는 경로
    6. 수익 모델(Revenue Streams): 수익을 창출하는 방식
    7. 비용 구조(Cost Structure): 주요 비용 항목
    8. 핵심 지표(Key Metrics): 성공을 측정할 수 있는 지표
    9. 불공정한 경쟁우위(Unfair Advantage): 경쟁자가 쉽게 모방할 수 없는 경쟁 우위
      린캔버스 템플릿

-Prettier 설정

  • Prettier는 코드 포맷터로, JavaScript, TypeScript, HTML, CSS 등 다양한 프로그래밍 언어의 코드를 일관된 스타일로 자동 정렬해주는 도구이다.
  • 코드의 가독성을 높이고, 팀 내 코딩 스타일을 통일할 수 있다.

-ESLint 설정

  • ESLint는 JavaScript와 관련된 코드를 분석하여 코드 품질과 일관성을 유지하기 위한 도구 
  • ESLint는 다양한 규칙을 설정할 수 있으며, 필요에 따라 사용자 정의 규칙도 추가할 수 있다
  1. 코드 품질 검사: 코드에서 잠재적인 오류나 버그를 찾아내고 경고를 표시합니다.
  2. 코딩 스타일 일관성: 설정된 규칙에 따라 코드 스타일을 강제하여 팀 내 코딩 스타일을 통일합니다.
  3. 자동 수정: 일부 문제는 자동으로 수정할 수 있습니다.

 

ESLint, Prettier 차이점 정리 

  • ESLint :  ESLint 는 코드 검사기로 코드에 에러가 있는지 검사해주 도구
  • Prettier : Prettier 는 코드 포매터로 코드를 일관성있고 예쁘게 정렬해 주는 도구

 

-eslint-config-prettier 설정

ESLint와 Prettier를 함께 사용할 때 동일한 코드 스타일 규칙을 다르게 정의할 수 있어 충돌이 발생할 수 있기 때문에 이를 방지하기 위한 ESLint 설정

  1. 충돌 방지: Prettier와 충돌할 수 있는 ESLint의 스타일 관련 규칙을 비활성화한다.
  2. 설정 간소화: 별도로 충돌을 해결하기 위한 추가 설정 없이, Prettier의 포맷팅 규칙을 우선적으로 적용할 수 있게 해준다.

 

 

섹션 15. 리액트 CSS 스타일링

-css 모듈

  • css모듈은 말그대로 css를 자바스크립트 모듈처럼 사용 가능하게 함
  • 리액트에서 css모듈을 사용하면 클래스 이름이 고유하게 변환되어 렌더링 됨( 이는 CSS 클래스 이름의 충돌을 방지 위함 )
  • 예시 - 두 카드의 css클래스 이름이 모두 card여도 styles 컴포넌트 사용하여 동적으로 스타일 할당
import styles from './Card1.module.css';

function Card1() {
  return <article className={styles.card}>Card1</article>;
}
export default Card1;
import styles from './Card2.module.css';

function Card2() {
  return <article className={styles.card}>Card2</article>;
}
export default Card2;

 

 

- React에서 self-closing 태그를 자동

  • React에서 self-closing 태그를 자동으로 해주는 ESLint 룰은 JSX에서 태그가 내용이 없을 때 자가 닫힘(self-closing) 태그로 강제로 작성되도록 한다.
  • ESLint 설정 파일인 .eslintrc또는 eslintConifig항목에 다음과 같이 추가
{
  "rules": {
    'react/self-closing-comp': 'warn'
  }
}

 

 

-Tailwind CSS란?

  • TailwindCSS는 유틸리티 클래스 기반의 CSS 프레임워크
  • 개발자가 HTML 클래스 속성에 클래스 명을 작성 함으로써 바로 스타일을 적용할 수 있도록 해준다. 이로 인해 CSS 파일을 작성할 필요 없이 간편하게 스타일을 관리할 수 있다.
  • TailwindCSS는 다른 CSS 프레임워크들과 다르게, 사전 정의된 컴포넌트나 스타일이 아닌, 유틸리티 클래스를 제공하여 더 유연하고 맞춤화된 디자인을 쉽게 구현한다.
  • 설치 가이드 : https://v3.tailwindcss.com/docs/guides/vite 
  • TailwindCSS의 장점
    1. 빠른 스타일링:
      • 유틸리티 클래스를 사용해 즉시 스타일을 적용할 수 있어, 개발 속도가 빨라짐
    2. 높은 일관성:
      • 동일한 유틸리티 클래스를 사용함으로써, 프로젝트 전반에 걸쳐 일관된 디자인을 유지할 수 있음
    3. 손쉬운 유지보수:
      • CSS가 HTML 내부에 포함되어 있어, 스타일을 변경하거나 업데이트할 때 HTML 파일만 수정하면 됨
    4. 반응형 디자인:
      • TailwindCSS는 반응형 디자인을 지원하는 다양한 클래스(sm:, md:, lg:, xl: 등)를 제공하여, 다양한 화면 크기에서도 쉽게 스타일을 적용할 수 있음
    TailwindCSS의 단점
    1. 긴 HTML 코드:
      • 유틸리티 클래스를 많이 사용하다 보면 HTML 코드가 길어져서 가독성에 악화시킬 수 있음
    2. 학습 곡선:
      • TailwindCSS의 다양한 클래스 이름과 사용법을 익히는 데 시간이 필요할 수 있음
    3. 초기 설정:
      • 프로젝트에 TailwindCSS를 설정하는 초기 단계가 다른 CSS 프레임워크보다 복잡할 수 있음

 

-요약

  • CSS Module: 빠른 로딩 속도와 클래스 충돌 방지가 필요할 때 적합하지만, 클래스명 관리를 신경 써야 하고 테마 변경이 어렵다.
  • Styled Components: 동적 스타일링과 컴포넌트 단위 스타일 관리가 필요한 경우에 적합하지만, 초기 로딩 시간이 길어질 수 있다.
  • Tailwind CSS: 빠른 개발 속도와 일관된 스타일 유지가 중요한 경우에 적합하지만, 유틸리티 클래스 사용에 익숙해져야 한다.
  • MUI (Material-UI): 일관된 디자인 시스템과 손쉬운 테마 변경이 필요한 경우에 적합하지만, 동적 스타일 적용 시 성능 저하가 발생할 수 있다.

 

 

섹션 16. React Router

-React Router 란?

  • React 애플리케이션에서 클라이언트 측 라우팅을 관리하는 라이브러리
  • 라우팅은 웹사이트에서 사용자가 어떤 페이지로 이동할지를 정하는 것 즉, 주소에 따라 웹사이트에서 보여줄 내용을 정리하는 것
  • 설치 코드
npm install react-router-dom
npm install react-icons --save

 

import React from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import './index.css';
import App from './App';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
const router = createBrowserRouter([
  {
    path: '/',
    element: <App />,
    children: [
      {
        path: '',
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
      {
        path: 'contact',
        element: <Contact />,
      },
    ],
  },
]);
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
);

Children에 있는 path는 상위컴포넌트의 path에 이어지기 때문에 상대경로로 작성해야함

 

 

 

섹션 17. UI 만들기

-헤더 UI

  • react-icons : 다양한 아이콘 라이브러리를 React 컴포넌트 형태로 사용할 수 있게 해주는 패키지
  • 설치 방법
npm install react-icons --save
  • Font Awesome (v5) 아이콘
import {
  FaHome,	//홈 화면 이동 버튼
  FaInfoCircle,	//정보 원형 아이콘
  FaEnvelope,	//연락처(Contact) 페이지
  FaBars,	//햄버거 메뉴 (세 줄 메뉴 아이콘)
  FaTimes,	//X 아이콘(닫기 버튼)
} from 'react-icons/fa';

 

 

-린캔버스 목록 UI

기능 설명
검색 입력한 텍스트로 제목 필터링
보기 전환 버튼 클릭으로 그리드/리스트 토글
빈 목록 처리 조건부 메시지 렌더링
반응형 디자인 Tailwind를 사용한 반응형 그리드
아이콘 사용 react-icons의 FaSearch, FaTh, FaList 활용
const [searchText, setSearchText] = useState('');
const [isGridView, setIsGridView] = useState(true);

 

  • searchText : 검색창에 입력된 텍스트를 저장
  • isGridView : 현재 목록이 그리드 뷰인지 아닌지 판별 (true: 그리드, false: 리스트)

 

const dummyData = [ ... ]

id, title, lastModified, category 등의 필드로 구성된 프로젝트 정보가 담겨 있음

 

const filteredData = dummyData.filter(item =>
  item.title.toLowerCase().includes(searchText.toLowerCase())
);

 

    • filter() 이용하여 검색 필터링 기능
    • 한 글자만 일치해도 include함수의 반환값이 true가 되므로 필터링 조건을 만족함
    • 아무것도 입력 안했는데 카드 떠 있는 이유는? → Include는 text가 빈값일때도 true반환하기 때문

 

{filteredData.length === 0 ? (
  <div>검색 결과가 없습니다</div>
) : (
  <div className={`grid ...`}>
    {filteredData.map(item => ( ... ))}
  </div>
)}

 

  • 검색 결과가 없을 경우엔 "검색 결과가 없습니다" 메시지 출력
  • 있으면 .map()으로 각각의 카드 UI 렌더링

 

<div className={`grid gap-6 ${isGridView ? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3' : 'grid-cols-1'}`}>

 

  • isGridView에 따라 Tailwind의 그리드 컬럼 수가 달라짐
  • 그리드 뷰: 반응형 다단 컬럼 (grid-cols-1, sm:grid-cols-2, lg:grid-cols-3)
  • 리스트 뷰: 한 줄(grid-cols-1)

 

 

-UI 컴포넌트 분리

컴포넌트를 분리하면 재사용이 가능하다!

Home.jsx
├── SearchBar         // 검색창 (입력 UI)
├── ViewToggle        // 보기 방식 버튼 (토글 UI)
└── CanvasList        // 목록 렌더링
     └── CanvasItem   // 단일 카드 렌더링
  • SearchBar.jsx
    • 역할: 사용자의 검색어 입력을 처리
    • props
      • searchText: 현재 입력된 검색어
      • setSearchText: 상태 업데이트 함수
    • 아이콘: FaSearch 사용
  • ViewToggle.jsx
    • 역할: 그리드/리스트 뷰 토글 UI
    • props
      • isGridView: 현재 뷰 상태 (true/false)
      • setIsGridView: 뷰 상태 변경 함수
    • 아이콘 : FaTh: 그리드 뷰 / FaList: 리스트 뷰
  • CanvasItem.jsx
    • 역할: 하나의 프로젝트 카드 형태 렌더링
    • props : id, title, lastModified, category
    • CanvasList 내부에서 반복적으로 호출
  • CanvasList.jsx
    • 역할: 필터된 데이터를 받아 각각을 CanvasItem으로 렌더링
    • props
      • filteredData: 검색 필터링 결과 배열
      • searchText: 현재 검색어 (빈 목록 메시지 출력을 위해 사용)
      • isGridView: 현재 보기 형식 (그리드/리스트)

 

 

-삭제버튼 UI 

각 항목 카드 오른쪽 상단에 삭제 버튼을 추가해서 해당 항목을 dummyData에서 제거하기

  • CanvasItem.jsx - 삭제 버튼 UI & 클릭 이벤트
<button
  className="absolute top-2 right-2 p-2 text-red-500 rounded-full"
  aria-label="Delete"
  onClick={onDelete}
>
  <FaTrash />
</button>

 

 

단, 부모가 Link이기 때문에 버튼 클릭 시 페이지 이동이 발생(이벤트 전파)할 수 있어서 상위 컴포넌트에서 e.preventDefault() 처리 필요

 

  • CanvasList.jsx - 각 아이템에 삭제 핸들러 전달
onDelete={e => {
  e.preventDefault(); // 링크 클릭 방지
  onDeleteItem(item.id); // 부모(Home)로 id 전달
}}

 

  • onDelete 이벤트에서 e.preventDefault()로 Link로의 이동 방지
  • item.id를 onDeleteItem에 전달하여 Home.jsx에서 데이터 삭제 수행
이벤트 전파를 막는 메서드는 e.stopPropagation이지만 이동을 할 때 링크 컴포넌트의 to속성으로 이동을 했고 이러한 링크 컴포넌트는 렌더링 된 시점에 앵커태그가 되기 때문에 기본동작을 막는 메서드로도 작동 가능
  • Home.jsx - 실제 데이터 삭제를 처리하는 로직
const [dummyData, setDummyData] = useState([...]);

const handleDeleteItem = id => {
  setDummyData(dummyData.filter(item => item.id !== id));
};

...

<CanvasList
  filteredData={filteredData}
  ...
  onDeleteItem={handleDeleteItem}
/>
  • handleDeleteItem은 id를 받아 해당 항목을 제외하고 상태를 갱신함
  • CanvasList에 삭제 핸들러 onDeleteItem을 props로 전달

 

 

 

 

-타이틀 컴포넌트

Lean Canvas 타이틀을 평소엔 <h1>으로 보여주고 +✏️ (편집 아이콘), 수정 버튼을 누르면 <input> 필드로 바뀌고 +✅ (저장 아이콘), 저장하면 텍스트가 업데이트되며 다시 보기 모드로 돌아감

const [title, setTitle] = useState('Lean Canvas');        // 실제 보여질 제목
const [editedTitle, setEditedTitle] = useState(title);    // 수정 중인 임시 제목
const [isEditing, setIsEditing] = useState(false);        // 편집 모드 여부
const handleEditTitle = () => setIsEditing(true);             // 편집 시작
const handleTitleChange = e => setEditedTitle(e.target.value); // 입력 업데이트
const handleTitleSubmit = () => {
  setTitle(editedTitle);     // 저장
  setIsEditing(false);       // 보기 모드로 전환
};
{isEditing ? (
  // input 필드 + 저장 버튼
) : (
  // h1 제목
)}

 

  • isEditing 상태에 따라 두 가지 UI를 전환
  • 보기 모드일 때만 편집 버튼 (FaEdit) 보이도록 조건부 처리

 

 

 

-레이아웃 UI

구현 화면

 

CanvasDetail.jsx
├── CanvasTitle          // 상단 제목 (편집 가능)
└── LeanCanvas           // 캔버스 그리드 UI
     └── CanvasCard      // 개별 섹션 카드 컴포넌트

 

  • LeanCanvas.jsx- 전체 캔버스 그리드 영역을 구성
import CanvasCard from './CanvasCard';
function LeanCanvas() {
  return (
    <div className="border-4 border-black">
      <div className="grid grid-cols-5">
        <CanvasCard title={'1. 문제'} />
        <CanvasCard title={'4. 해결안'} />
        <CanvasCard title={'3. 가치제안'} />
        <CanvasCard title={'5. 경쟁우위'} />
        <CanvasCard title={'2. 목표 고객'} />
        <CanvasCard title={'기존 대안'} isSubtitle />
        <CanvasCard title={'8. 핵심지표'} />
        <CanvasCard title={'상위개념'} isSubtitle />
        <CanvasCard title={'9. 고객 경로'} />
        <CanvasCard title={'얼리 어답터'} isSubtitle />
      </div>
      <div className="grid grid-cols-2">
        <CanvasCard title={'7. 비용 구조'} />
        <CanvasCard title={'6. 수익 흐름'} />
      </div>
    </div>
  );
}

export default LeanCanvas;
  • 10개 영역은 5열 구조 (grid-cols-5)
  • 하단의 2개 영역은 2열 구조 (grid-cols-2)
  • 각각은 CanvasCard로 표현됨
  • CanvasCard.jsx- 전체 캔버스 그리드 영역을 구성
import { FaPlus } from 'react-icons/fa';
function CanvasCard({ title, isSubtitle = false }) {
  return (
    <div className="row-span-1 bg-white min-h-48 border border-collapse border-gray-300">
      <div
        className={`${isSubtitle === false && 'bg-gray-100 border-b border-b-gray-300'} flex items-start justify-between px-3 py-2`}
      >
        <h3 className={`${isSubtitle === false && 'font-bold'} `}>{title}</h3>
        <button className="bg-blue-400  text-white p-1.5 text-xs rounded-md">
          <FaPlus />
        </button>
      </div>
      <div className="space-y-3 min-h-32 p-3">memo..</div>
    </div>
  );
}

export default CanvasCard;

 

 

 

-메모 추가 및 제거 UI

CanvasCard 안에서 ➕ 버튼 클릭 시 새로운 메모(Note) 추가 ❌ 버튼 클릭 시 해당 메모 삭제하기

  • CanvasCard.jsx - 메모 목록 관리 컴포넌트
const [notes, setNotes] = useState([]);

...

const handleAddNote = () => {
  setNotes([...notes, { id: uuidv4(), content: '' }]);
};
  • uuidv4()를 사용해 고유 ID를 생성 -uuid는 유니크한 식별자를 생성하기 위한 라이브러리. 랜덤 기반이라서 매번 새로 호출하면 다른 ID가 나옴
const handleRemoveNote = id => {
  setNotes(notes.filter(note => note.id !== id));
};

삭제 버튼 클릭 시 해당 ID를 제외한 note만 유지

 

{notes.map(note => (
  <Note
    key={note.id}
    id={note.id}
    content={note.content}
    onRemoveNote={handleRemoveNote}
  />
))}

 

각각의 메모는 Note 컴포넌트로 분리되어 있음

 

  • Note.jsx - 개별 메모 아이템 컴포넌트
 
function Note({ id, onRemoveNote }) {
  return (
    <div className="border border-black">
      <button onClick={() => onRemoveNote(id)}>
        <AiOutlineClose size={20} />
      </button>
    </div>
  );
}
  • ❌ 버튼을 눌렀을 때 id를 부모로 전달해서 삭제 요청
  • 현재는 내용(content)은 렌더링하거나 수정하지 않고 삭제만 가능