
이 게시글은 현업에서 사용하는 React 고급 웹 개발 기법 배우기 (Feat. TodoList) 시리즈중 하나입니다
- Vite 시작하기, TailwindCss로 TodoList UI 구성하기
- React Hook 활용하여 TodoList 기능 개발하기, github page로 배포하기
- MSW로 API Mocking하기, React Query 사용하기
- React Testing Library & Jest로 테스트 하기. CI 적용하기
1. 프로젝트 생성(Create Vite)
다음 명령어를 입력하여 새로운 vite 프로젝트를 만들어봅시다!
npm create vite@latest
프로젝트 이름을 적어주세요~
프레임워크를 선택해주세요~ 저희는 React로 하겠습니다
Typescript 또는 Typescript + SWC로 해주세요~
다음 명령어를 입력해서 프로젝트 의존성을 설치한 뒤에 실행해봅시다
pnpm i
pnpm dev
http://localhost:5173에 접속해봅니다.
그러면 다음과 같이 기본적인 카운터가 보이실겁니다!
와 정말 간단하고 빠르죠? 이제 다른 여러 설정을 해봅시다
2. 프로젝트 기본 세팅
다음 명령어로 vs code에서 편집할 예정입니다
code todo-list
├── public
│ └── vite.svg
├── src
│ ├── App.css
│ ├── App.tsx
│ ├── assets
│ │ └── react.svg
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
├── README.md
├── index.html
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
주요 파일만 보면서 세팅을 진행해보죠
package.json
{
"name": "todo-list",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}
필요한 것만 딱 있고 dev dependencies는 따로 정리되어있는게 너무 마음에 들죠~ 지금은 딱히 건드릴게 없으니 넘어갑시다. vite에서는 프로젝트 시작을 dev로 합니다!
vite.config.ts
vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
특이하게 json, js등이 아니고 ts 파일로 설정을 할 수 있죠? vite의 장점인데요 옵션에 타입이 추론되면서 자동완성을 활용해서 설정을 쉽게 할 수 있습니다!
시작 포트가 5173이라서 당황하셨을 수도 있을 것 같아요 다음 설정을 해줍시다
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: "localhost",
port: 3000,
},
});
바로 서버가 재실행되는 것을 보실 수 있습니다!
기존에 CRA로 개발했을 떄는 이런 설정이 변경되면 리액트 서버를 껏다 켜야되는 경우가 많았는데요. vite는 빠르게 다시 재실행 해줍니다!
추가 실습을 위해 다음 플러그인을 설치해봅시다!
pnpm add vite-tsconfig-paths
그리고 plugins 배열에 다음을 추가해줍니다.
vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths()],
server: {
host: "localhost",
port: 3000,
},
});
이 플러그인은 tsconfig의 path alias를 자동을 적용해주는 플러그인입니다!
tsconfig.json
{
"compilerOptions": {
...
"baseUrl": "./src",
"paths": {
"@components/*": ["components/*"]
}
},
...
}
3. UI 기획하기
저희가 개발해볼 Todo List는 다음 프로젝트의 스펙을 따를 겁니다.
TodoMVC
바로 TodoMVC라는 다양한 프레임워크로 투두리스트를 만드는 프로젝트에서 참고할 예정입니다!
직접 접속해보셔서 여러 기능을 체험해보세요!
오늘은 UI를 구성해야 하기 때문에 UI를 중점으로 살펴봐야 합니다.
저는 UI를 개발할 때는 다음 4가지를 신경써서 개발 합니다.
1. 예외 처리하기
예상치 못한 입력과 데이터에 대해 대비해야 하며 UI단에서 이를 충분히 처리해줘야 합니다.
물론 간단한 투두리스트여서 자세하게 신경 쓸 필요는 없겠지만 적어도 이정도는 신경써줘야 할겁니다.
- 투두가 하나도 없을 때 UI
- 투두가 너무 많아서 화면을 벗어날 때 UI
2. 상태에 따라 달라지는 UI처리하기
UI를 개발할 때 미리 상태에 따라 달라질 요소들을 처리를 해두어야 나중에 로직을 붙여도 편합니다.
예를들어 다음 요소가 있습니다.
- 투두를 눌렀을 때 디자인
- 투두 삭제 버튼은 언제 등장 시킬지
- Active를 눌렀을 때 완료되지 않은 투두만 보이게 하기
3. 유저 인터렉션 신경쓰기
유저와 상호작용을 밀접하게 할 요소들에 대해 처리를 잘 해주어야 합니다.
예를들어 투두 토글 버튼이나 Active 탭 같은 부분들은 상호작용 가능한 요소라는 인식을 확실히 주어야 하죠
4. 박스 단위로 계층 구조 설계하기
모든 UI는 박스 단위로 구성할 수 있습니다. 가장 중요한 기본 원리이며 이 규칙에 따라 컴포넌트를 설계해보겠습니다.
4. 데이터 정의하기
UI를 구성할 때 필수로 필요한 것은 디자인과 데이터입니다. UI 개발의 완성(결과)된 모습은 디자인에 따르는 것이고 UI를 개발하는 과정은 데이터의 구조에 민감하게 영향을 받습니다.
우리는 이미 투두리스트의 디자인을 알고 어느 기능이 필요한지 어느정도 알고있으니 다음과 같이 데이터 구조를 구성해볼 수 있을 것입니다.
interface Todo {
id: string;
content: string;
completed: boolean;
}
type TabState = "All" | "Active" | "Completed"
Todo에 id도 만들어 놓은 이유는 우리가 Todo를 삭제하고 토글하는 로직에 필요하기 때문입니다.
하단에 탭 또한 상태로 볼 수 있겠죠?
5. 본격 UI 개발하기
이번에는 tailwind를 통해 스타일링을 할 계획입니다.
물론 처음 하시는 분들이라면 익숙하지 않으실 수 있으나 잘 따라오시면 문제 없습니다. 한번 배워보시는 것도 추천합니다!.
tailwind를 쓰는 이유는 css in js 대비하여 성능이 월등히 좋고, 스타일링을 하며 클래스 네이밍을 따로 할 필요가 없으며, 무엇보다 인라인으로 스타일을 작성하기 때문에 편하기 때문입니다.
스타일링 방식의 장단점에 대해서는 나중에 따로 다루도록 하겠습니다.
종속성 설치
다음 가이드를 보면 vite 프로젝트에 tailwind를 적용하는 방법을 가이드하고 있습니다.
https://tailwindcss.com/docs/guides/vite
1. tailwind css 설치
pnpm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
tailwind.config.js이 생성되었을 겁니다.
2. 설정하기
tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
3. tailwind directives 삽입하기
src/index에 모든 내용을 지우고 다음 3줄을 삽입합니다.
@tailwind base;
@tailwind components;
@tailwind utilities;
끝!
컴포넌트 설계
다음과 같이 Box를 밖에서부터 안으로 계층 구조를 가지며 그려주었습니다.
컴포넌트의 구조와 명칭 자체로 컴포넌트의 역할이 명확히 분리되는 것을 볼 수 있습니다.
TodoList를 관리하는 핵심 컴포넌트는 TodoList.tsx로 만들어줄 것이고 App.tsx는 그 투두리스트를 띄워주는 역할을 할 것입니다.
컴포넌트 작성
라이브러리 설치
먼저 휴지통이나 체크 부분의 아이콘들은 우리가 직접 만들기보다는 라이브러리를 사용해봅시다!
pnpm add react-ioncs
다음 홈페이지에서 마음에 드는 아이콘을 찾아 쓸 수 있습니다!
React Icons
font awesome의 다음 아이콘들을 써보죠!
App.tsx
다 지워주고 다시 작성해줄겁니다
import { useState } from "react";
import TodoList from "@components/TodoList";
export interface Todo {
id: string;
content: string;
completed: boolean;
}
export type TabState = "All" | "Active" | "Completed";
function App() {
const [todos, setTodos] = useState<Todo[]>([
{
id: "1",
content: "Todo 1",
completed: false,
},
{
id: "2",
content: "Todo 2",
completed: true,
},
{
id: "3",
content: "Todo 3",
completed: false,
},
]);
const [currentTab, setCurrentTab] = useState<TabState>("All");
return (
<div>
<h1>todos</h1>
<TodoList todos={todos} currentTab={currentTab} />
</div>
);
}
export default App;
TodoList는 주어진 todos 배열을 받아 투두리스트 기능을 해주는 것이고 데이터의 관리는 App에서 합니다.
App.css는 날려주세요!
components/TodoList/index.tsx
import { type TabState, type Todo } from "App";
import Header from "./header";
import TodoItem from "./TodoItem";
import Footer from "./footer";
interface TodoProps {
todos: Todo[];
currentTab: TabState;
}
const TodoList = ({ todos, currentTab }: TodoProps) => {
return (
<div>
<Header />
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
<Footer todos={todos} currentTab={currentTab} />
</div>
);
};
export default TodoList;
components/TodoList/Header.tsx
import { FaCheck } from "react-icons/fa";
const Header = () => {
return (
<div>
<button>
<FaCheck />
</button>
<input type="text" />
</div>
);
};
export default Header;
components/TodoList/TodoItem.tsx
import { type Todo } from "App";
import { FaRegTrashAlt, FaRegCheckCircle, FaRegCircle } from "react-icons/fa";
interface TodoItemProps {
todo: Todo;
}
const TodoItem = ({ todo }: TodoItemProps) => {
return (
<div>
{todo.completed ? (
<button>
<FaRegCheckCircle />
</button>
) : (
<button>
<FaRegCircle />
</button>
)}
<p>{todo.content}</p>
<button>
<FaRegTrashAlt />
</button>
</div>
);
};
export default TodoItem;
components/TodoList/Footer.tsx
import { type TabState, type Todo } from "App";
interface FooterProps {
todos: Todo[];
currentTab: TabState;
}
const Footer = ({ todos }: FooterProps) => {
return (
<div>
<p>{todos.filter((todo) => !todo.completed).length} left</p>
<div>
<button>All</button>
<button>Active</button>
<button>Completed</button>
</div>
{todos.some((todo) => todo.completed) && <button>Clear completed</button>}
</div>
);
};
export default Footer;
한번에 너무 완벽하게 할 필요는 없습니다. 다만 컴포넌트의 구조를 미리 생각을 해놓고 작성하니 훨씬 수월하죠
사실 실제로 개발할 때는 스타일링을 하면서 컴포넌트를 작성 하는데요, 가이드를 작성하다 보니 챕터를 나누게 되었습니다.
결과물을 보니 처참하죠? 이번 시간에 스타일링을 전부 끝내 놓읍시다!
스타일링
재미있는 스타일링 시간이네요~ flex를 잘 활용해서 디자인 해봅시다
먼저 App.css를 날려주세요!
App.tsx
<div className="w-screen h-screen flex-col gap-4 flex items-center bg-stone-100">
<h1 className="font-serif font-light text-6xl mt-8 text-stone-400">
todos
</h1>
<TodoList todos={todos} currentTab={currentTab} />
</div>
색상은 stone 색상으로 맞출 예정입니다.
components/TodoList/index.tsx
<div className="w-[600px] max-h-[calc(100vh-200px)] flex flex-col bg-white rounded-lg drop-shadow-md">
<Header />
<div className="h-full overflow-y-auto">
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
<Footer todos={todos} currentTab={currentTab} />
</div>
투두가 많이 질 경우를 대비하여 max height를 걸어주고 스크롤 처리해줍니다.
components/TodoList/Header.tsx
<div className="flex gap-2 items-stretch px-4 h-16 focus-within:outline focus-within:outline-2 focus-within:outline-blue-300 border-b-2 border-stone-300 shrink-0">
<button className="w-8 flex items-center justify-center hover:text-stone-600 text-stone-400">
<FaCheck />
</button>
<input
type="text"
className="flex-1 focus:outline-none text-lg placeholder:italic"
placeholder="What needs to be done?"
/>
</div>
버튼과 같이 상호작용 가능한 요소들은 hover할 시에 색상을 다르게 처리를 해줍니다.
input같은 요소는 focus시에 header 전체가 focus를 유지할 수 있도록 접근성을 제공합니다.
components/TodoList/TodoItem.tsx
<div className="px-4 gap-2 group flex items-center h-16 border-b-[1px] border-stone-300 shrink-0">
{todo.completed ? (
<button className="flex w-8 justify-center items-center">
<FaRegCheckCircle className="w-6 h-6 text-stone-400 hover:text-stone-600" />
</button>
) : (
<button className="flex w-8 justify-center items-center">
<FaRegCircle className="w-6 h-6 text-stone-400 hover:text-stone-600" />
</button>
)}
<p
className={`flex-1 text-lg ${
todo.completed ? "text-stone-400 line-through" : "text-stone-600"
}`}
>
{todo.content}
</p>
<button className="group-hover:visible invisible w-8">
<FaRegTrashAlt className="text-red-500 w-5 h-5" />
</button>
</div>
패딩은 모두 가로 16px을 넣습니다. 삭제 버튼은 hover시에만 보이게 해줍니다.
components/TodoList/Footer.tsx
<div className="flex px-4 items-center h-12 justify-center shrink-0 border-t-[1px] border-stone-300">
<p className="absolute left-4 text-sm text-stone-500">
{todos.filter((todo) => !todo.completed).length} item left
</p>
<div className="flex gap-4">
{(["All", "Active", "Completed"] as TabState[]).map((tab) => (
<button
key={tab}
className={`p-1 text-lg hover:text-stone-600 ${
currentTab === tab
? "outline outline-1 outline-stone-300 text-stone-600"
: "text-stone-400"
}`}
>
{tab}
</button>
))}
</div>
{todos.some((todo) => todo.completed) && (
<button className="absolute right-4 text-stone-400 hover:text-stone-600">
Clear completed
</button>
)}
</div>
currentTab의 상태에 따른 스타일링도 미리 처리해줍니다.
tailwind를 처음 보신 분들이라면 좀 어지러우실 수 있습니다.. 최대 단점이죠
너무 따라오시기 어려우신 분들은 복사 하시고 하나 하나 왜 이런 스타일을 넣었는지 보시면서 이해해보시면 좋을 것 같습니다.
그러면 다음과 같이 스타일링을 완료 했습니다!
결과 디렉토리
├── public
│ └── vite.svg
├── src
│ ├── App.tsx
│ ├── components
│ │ └── TodoList
│ │ ├── TodoItem.tsx
│ │ ├── footer.tsx
│ │ ├── header.tsx
│ │ └── index.tsx
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
├── README.md
├── index.html
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
└── vite.config.ts
마치며
이번 게시글을 통하여 제가 UI를 개발할 때 어떤 과정을 거치는지, 무엇을 고려하는지 전달해드리고 싶었습니다.
이번에는 tailwind를 소개드렸는데요 강력한 기능과 성능, 편의성으로 많은 인기를 끌고 있는 스타일링 라이브러리입니다. 처음 보신 분들은 이번 기회에 한번 배워보시는 것도 좋을 것 같네요.
다만 무엇이든 새로운 기술을 배울 때 이 기술을 왜 등장 하였고 왜 써야 하는지 생각해보는 시간을 가지시면 좋을 것 같습니다.
'FrontEnd' 카테고리의 다른 글
React Testing Library & Vitest로 테스트 하기, CI 적용하기 (0) | 2024.08.18 |
---|---|
MSW로 API Mocking하기, React Query 사용하기 (1) | 2024.03.17 |
React Hook 활용하여 TodoList 기능 개발하기, github pages로 배포하기 (2) | 2024.01.25 |
현업에서 사용하는 React 고급 웹 개발 기법 배우기 (Feat. TodoList) (2) | 2024.01.20 |