UMC 8th Web 워크북

🌐TodoList - ts + vite + yarn 세팅(tailwind css 적용.)

minnote29 2025. 3. 29. 16:57
더보기

지난번에 todolist를 타입스크립트로 어렵게 구현하고, 스타일 컴포넌트로 구현했는데 여러 한계점이 있었다.

듣기로는 스타일 컴포넌트 같은 경우, 많은 개발자들이 쓰고 있다고 생각했는데 버전 업데이트를 더이상 하지 않는다고 해서 큰 충격이었다. 그래서 tailwind css로 대체하여 쓸 생각이다.

📌 pnpm 설치 (pnpm으로 하고 싶을 때.)

https://pnpm.io/ko/installation

 

설치하기 | pnpm

필수 구성 요소

pnpm.io

  • corepack을 먼저 설치해주고, 
  • $ npm install --global corepack@latest
  • 사전에 Node.js를 이미 깔았으므로
    $ npm install -g pnpm@latest-10
  • 이렇게 pnpm을 깔아줘야 한다. 

 

 

 

📌 yarn 설치하기 (yarn으로 하고 싶을 때.)

https://yarnpkg.com/getting-started/install

 

Installation | Yarn

Yarn's in-depth installation guide.

yarnpkg.com

$ npm install -g corepack
$ cd Min  // 만든 폴더로 이동.

 

$ yarn create vite 

vite 프로젝트 생성

$ cd TodoList

생성한 프로젝트로 이동

 

추가적인 패키지 설치를 위해 아래처럼 설치를 해줬다

$ npm i
$ npm i react-router-dom
$ yarn run dev  // 개발자 모드 실행.

 

 

📌 tailwind css 설치하기 (공식문서 참고.) - vite를 사용할 경우

https://tailwindcss.com/docs/installation/using-vite

 

Installing with Vite - Installation

Integrate Tailwind CSS with frameworks like Laravel, SvelteKit, React Router, and SolidJS.

tailwindcss.com

1. install tailwind css, @tailwindcss/vite via npm.

$ npm install tailwindcss @tailwindcss/vite

 

2. Configure the vite plugin - add the @tailwindcss/vite plugin to your vite configuration.

import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
  plugins: [
    tailwindcss(),
  ],
})
  • 예시가 이렇게 나와있어서, 내 프로젝트에서도 vite.config.ts를 바꿔줬다.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import tailwindcss from '@tailwindcss/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
  ],
})

 

3. import tailwind css - @import해서 css 파일에 넣어줬다. 

@import "tailwindcss";

-> index.css와 App.css에 있는 기존 내용은 전부 지우고, index.css 파일에 넣어줬다.

@import "tailwindcss";

 

4. start your build process

$ npm run dev

 

5. html에 tailwind를 적용해보라고 했지만, 리액트로 하고 있었기에 Styles.ts 파일을 만들고 "classes"라는 함수를 만들어서 TodoList의 각 컴포넌트들의 className에 따라 적용을 했다.

export const classes = {
  container: `
    bg-white
    rounded-[15px]
    shadow-md
    p-6
    w-[500px]
    text-center
  `,

  title: `
    text-[24px]
    font-bold
    mb-6
  `,

  inputContainer: `
    flex
    justify-between
    items-center
    mb-6
  `,

  input: `
    flex-1
    border
    border-gray-300
    rounded-md
    px-4
    py-2
    mr-2
    outline-none
  `,

  wrapper: `
    flex
    justify-between
    gap-4
  `,

  listContainer: `
    flex-1
  `,

  completedContainer: `
    flex-1
  `,

  titleSection: `
    font-bold
    text-lg
    mb-3
  `,

  itemContainer: `
    bg-gray-100
    rounded-md
    p-3
    mb-2
    flex
    items-center
    justify-between
  `,

  itemText: `
    text-left
  `,

  addButton: `
    bg-green-500
    text-white
    px-4
    py-2
    rounded-md
  `,

  completeButton: `
    bg-green-500
    text-white
    px-3
    py-1
    rounded-md
  `,

  deleteButton: `
    bg-red-500
    text-white
    px-3
    py-1
    rounded-md
  `,
};

 

Todo.tsx - 컴포넌트 전체를 이루는 상위 컴포넌트

import React from "react";
import TodoInput from "./components/TodoInput";
import TodoList from "./components/TodoList";
import CompletedList from "./components/CompletedList";
import { TodoProvider } from "../../contexts/TodoContext";
import { classes } from "./Styles";

const Todo: React.FC = () => {
  return (
    <div className="min-h-screen flex justify-center items-center bg-gray-100">
      <TodoProvider>
        <div className={classes.container}>
          <h2 className={classes.title}>UMC TODOLIST</h2>
          <TodoInput />
          <div className={classes.wrapper}>
            <TodoList />
            <CompletedList />
          </div>
        </div>
      </TodoProvider>
    </div>
  );
};

export default Todo;

 

TodoList.tsx - 할 일에 관한 목록 -> 완료 버튼 활성화

import React from "react";
import { useTodo } from "../../../contexts/TodoContext";
import TodoItem from "./TodoItem";
import { classes } from "../Styles";

const TodoList: React.FC = () => {
  const { todos, completeTodo } = useTodo();

  return (
    <div className={classes.listContainer}>
      <h2 className={classes.title}>할 일</h2>
      {todos.map((todo, index) => (
        <TodoItem key={index} text={todo} onComplete={() => completeTodo(index)} />
      ))}
    </div>
  );
};

export default TodoList;

 

TodoItem.tsx - 각 요소들에 대한 삭제, 완료 허용.

import React from "react";
import { classes } from "../Styles";

interface TodoItemProps {
  text: string;
  onComplete?: () => void;
  onDelete?: () => void;
}

const TodoItem: React.FC<TodoItemProps> = ({ text, onComplete, onDelete }) => {
  return (
    <div className={classes.itemContainer}>
      <span className={classes.itemText}>{text}</span>
      {onComplete && (
        <button onClick={onComplete} className={classes.completeButton}>
          완료
        </button>
      )}
      {onDelete && (
        <button onClick={onDelete} className={classes.deleteButton}>
          삭제
        </button>
      )}
    </div>
  );
};

export default TodoItem;

 

TodoInput.tsx - 입력창

import React, { useState } from "react";
import { useTodo } from "../../../contexts/TodoContext";
import { classes } from "../Styles";

const TodoInput: React.FC = () => {
  const [text, setText] = useState("");
  const { addTodo } = useTodo();

  const handleAddTodo = () => {
    if (text.trim()) {
      addTodo(text);
      setText("");
    }
  };

  return (
    <div className={classes.inputContainer}>
      <input
        type="text"
        placeholder="할 일 입력"
        value={text}
        onChange={(e) => setText(e.target.value)}
        onKeyUp={(e) => e.key === "Enter" && handleAddTodo()}
        className={classes.input}
      />
      <button onClick={handleAddTodo} className={classes.addButton}>
        할 일 추가
      </button>
    </div>
  );
};

export default TodoInput;

 

CompletedList.tsx - 완료 목록 -> 삭제 버튼 활성화

import React from "react";
import { useTodo } from "../../../contexts/TodoContext";
import TodoItem from "./TodoItem";
import { classes } from "../Styles";

const CompletedList: React.FC = () => {
  const { completedTodos, deleteTodo } = useTodo();

  return (
    <div className={classes.completedContainer}>
      <h2 className={classes.title}>완료</h2>
      {completedTodos.map((todo, index) => (
        <TodoItem key={index} text={todo} onDelete={() => deleteTodo(index)} />
      ))}
    </div>
  );
};

export default CompletedList;

 

 

📌Context 적용

TodoContext.tsx 파일을 만들고, 전역 상태 관리를 이루도록 했다.

import { createContext, useContext, useState, ReactNode } from "react";

interface TodoContextType {
  todos: string[];
  completedTodos: string[];
  addTodo: (text: string) => void;
  completeTodo: (index: number) => void;
  deleteTodo: (index: number) => void;
}

const TodoContext = createContext<TodoContextType | undefined>(undefined);

export const useTodo = () => {
  const context = useContext(TodoContext);
  if (!context) throw new Error("useTodo must be used within a TodoProvider");
  return context;
};

export const TodoProvider = ({ children }: { children: ReactNode }) => {
  const [todos, setTodos] = useState<string[]>([]);
  const [completedTodos, setCompletedTodos] = useState<string[]>([]);

  const addTodo = (text: string) => setTodos([...todos, text]);

  const completeTodo = (index: number) => {
    const newTodos = [...todos];
    const completed = newTodos.splice(index, 1)[0] ?? "";
    setTodos(newTodos);
    setCompletedTodos([...completedTodos, completed]);
  };

  const deleteTodo = (index: number) => {
    setCompletedTodos((prev) => prev.filter((_, i) => i !== index));
  };

  return (
    <TodoContext.Provider
      value={{ todos, completedTodos, addTodo, completeTodo, deleteTodo }}
    >
      {children}
    </TodoContext.Provider>
  );
};

 

1. interface TodoContextType

할 일 Context에서 사용할 상태와 함수들의 타입을 정의한 인터페이스이다.

  • todos: 현재 해야 할 일의 목록
  • completedTodos: 완료된 할 일의 목록
  • addTodo: 새로운 할 일을 추가하는 함수
  • completeTodo: 할 일을 완료 상태로 옮기는 함수
  • deleteTodo: 완료된 할 일을 삭제하는 함수

2. TodoContext

createContext를 통해 생성한 Context 객체로, 기본값은 undefined로 설정하여 Provider 외부에서 사용할 경우 에러가 발생하도록 하였다.

 

3. useTodo (커스텀 훅)

Context를 사용하기 위한 훅을 만든 것이고, 내부에서 useContext로 TodoContext를 사용하게 하고, TodoProvider 내부가 아닐 경우는 에러를 발생시키게 했고, 이 훅을 통해 어디서든 todos, addTodo 등의 함수에 접근할 수 있게 했다.

 

4. TodoProvider

Todo 관련 상태와 함수를 포함한 Provider 컴포넌트이다.

  • todos와 completedTodos를 useState로 관리하고, addTodo는 새 할 일을 todos에 추가하는 함수입니다.
  • completeTodo는 지정한 인덱스의 할 일을 완료 목록으로 옮기고, deleteTodo는 완료된 목록에서 할 일을 제거할 수 있게 해줬고,
  • TodoContext.Provider로 children을 감싸 상태를 전달했습니다.