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

린캔버스 템플릿
-Prettier 설정
- Prettier는 코드 포맷터로, JavaScript, TypeScript, HTML, CSS 등 다양한 프로그래밍 언어의 코드를 일관된 스타일로 자동 정렬해주는 도구이다.
- 코드의 가독성을 높이고, 팀 내 코딩 스타일을 통일할 수 있다.
-ESLint 설정
- ESLint는 JavaScript와 관련된 코드를 분석하여 코드 품질과 일관성을 유지하기 위한 도구
- ESLint는 다양한 규칙을 설정할 수 있으며, 필요에 따라 사용자 정의 규칙도 추가할 수 있다
- 코드 품질 검사: 코드에서 잠재적인 오류나 버그를 찾아내고 경고를 표시합니다.
- 코딩 스타일 일관성: 설정된 규칙에 따라 코드 스타일을 강제하여 팀 내 코딩 스타일을 통일합니다.
- 자동 수정: 일부 문제는 자동으로 수정할 수 있습니다.
ESLint, Prettier 차이점 정리
- ESLint : ESLint 는 코드 검사기로 코드에 에러가 있는지 검사해주 도구
- Prettier : Prettier 는 코드 포매터로 코드를 일관성있고 예쁘게 정렬해 주는 도구
-eslint-config-prettier 설정
ESLint와 Prettier를 함께 사용할 때 동일한 코드 스타일 규칙을 다르게 정의할 수 있어 충돌이 발생할 수 있기 때문에 이를 방지하기 위한 ESLint 설정
- 충돌 방지: Prettier와 충돌할 수 있는 ESLint의 스타일 관련 규칙을 비활성화한다.
- 설정 간소화: 별도로 충돌을 해결하기 위한 추가 설정 없이, 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의 장점
- 빠른 스타일링:
- 유틸리티 클래스를 사용해 즉시 스타일을 적용할 수 있어, 개발 속도가 빨라짐
- 높은 일관성:
- 동일한 유틸리티 클래스를 사용함으로써, 프로젝트 전반에 걸쳐 일관된 디자인을 유지할 수 있음
- 손쉬운 유지보수:
- CSS가 HTML 내부에 포함되어 있어, 스타일을 변경하거나 업데이트할 때 HTML 파일만 수정하면 됨
- 반응형 디자인:
- TailwindCSS는 반응형 디자인을 지원하는 다양한 클래스(sm:, md:, lg:, xl: 등)를 제공하여, 다양한 화면 크기에서도 쉽게 스타일을 적용할 수 있음
- 긴 HTML 코드:
- 유틸리티 클래스를 많이 사용하다 보면 HTML 코드가 길어져서 가독성에 악화시킬 수 있음
- 학습 곡선:
- TailwindCSS의 다양한 클래스 이름과 사용법을 익히는 데 시간이 필요할 수 있음
- 초기 설정:
- 프로젝트에 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)은 렌더링하거나 수정하지 않고 삭제만 가능
'프론트엔드' 카테고리의 다른 글
| [React] 영화 대여 온라인몰 확장 구현하기 (0) | 2025.05.04 |
|---|---|
| [React] 영화 목록 웹사이트 만들기 + 검색 기능 구현 (1) | 2025.04.27 |
| [React]React 완벽 마스터: 기초 개념부터 린캔버스 프로젝트까지 ② (0) | 2025.03.31 |
| [React]React 완벽 마스터: 기초 개념부터 린캔버스 프로젝트까지 ① (0) | 2025.03.22 |
| [HTML, CSS] 네이버 클론코딩 ② (0) | 2025.02.18 |