NextJS로 Todo 앱 만들기

시사



안녕하세요.
오늘은 NextJSSASS을 사용하여 작은 할 일 앱을 빌드하는 방법을 보여 드리겠습니다. 피드백이 있으면 댓글에 남겨주세요.

직접 시도해 볼 수 있습니다: Live Demo

코드 링크




파비안반디니 / todo-앱-튜토리얼


dev.to에 있는 튜토리얼의 소스 코드





튜토리얼 소스 코드
데모: https://todo-app-tutorial-f4p029hcs-fabianbandini.vercel.app/



View on GitHub



프로젝트 초기화




mkdir todo-app
cd todo-app
npm init -y
npm install sass next react react-dom


패키지.json 변경




{
  "name": "todo-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev":"next dev"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "next": "^0.0.3",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "sass": "^1.53.0"
  }
}


마지막으로 콘솔에 npm run dev를 입력하고 브라우저에서 localhost으로 이동합니다.

파일 구조 만들기





todo 앱용 로직 만들기




스타일/global.scss



* {
  font-family: sans-serif;
  padding: 0;
  margin: 0;
  background: #1A1B1E;
}




페이지/_app.js



import "../styles/global.scss";

export default function App({Component, pageProps}){
    return (
        <Component {...pageProps} />
    );
}




구성 요소/TodoList.js



export default function TodoList(){
    return (
        <>

        </>
    );
}


이 구성 요소는 사용자의 입력을 처리하고 새 작업을 생성합니다.

지금은 구성 요소만 만들 것입니다. 로직이 구현된 후 일부 스타일을 추가합니다.


구성 요소/TodoItem.js



import {useState} from "react";

export default function TodoItem({title, id, handleClickRemove}){
    const [done, setIsDone] = useState(false);

    const markDone = () => {
        setIsDone(!done);
    }

    return (
        <div onClick={markDone}>
            <h2>{title}</h2>
            <button onClick={() => handleClickRemove(id)}></button>
        </div>
    );
}


위에서 본 구성 요소는 우리의 할 일을 모델링하는 데 주의를 기울일 것입니다. TodoList.js로 돌아가서 todo를 추가하고 그에 따라 표시하는 논리를 만듭니다.


구성 요소/TodoList.js



import {useState} from "react";
import TodoItem from "./TodoItem";

export default function TodoList() {
    const [todos, setTodos] = useState([]);
    const [currentInput, setCurrentInput] = useState("");

    const handleClickAdd = (e) => {
        e.preventDefault();

        if (currentInput === "") {
            //input validation
            return;
        }

        const newTodo = {
            id: currentInput + Math.random().toString(),
            content: currentInput
        }

        setTodos([newTodo, ...todos]);
        setCurrentInput("")
    }

    const handleClickRemove = (id) => {
        const filteredTodos = todos.filter(todo => todo.id !== id);
        setTodos(filteredTodos);
    }

    return (
        <div>
            <form onSubmit={(e) => handleClickAdd(e)}>
                <input onChange={(e) =>
                    setCurrentInput(e.target.value)} 
                    value={currentInput}/>
                <button type={"submit"}>add</button>
            </form>
            <div>
                {
                    todos.map(todo => {
                        return (
                            <TodoItem title={todo.content}
                                      id={todo.id}
                                      handleClickRemove= 
                                      {handleClickRemove}/>
                        );
                    })
                }
            </div>
        </div>
    );
}


todos를 조금 더 보기 좋게 만들기 위해 css를 추가하기 전에 삭제 아이콘을 추가합니다. 내 here에서 얻을 수 있습니다.


구성 요소/TodoItem.js



import {useState} from "react";
import Image from "next/image";

export default function TodoItem({title, id, handleClickRemove}){
    const [done, setIsDone] = useState(false);

    const markDone = () => {
        setIsDone(!done);
    }

    return (
        <div onClick={markDone}>
            <h2>{title}</h2>
            <button onClick={() => handleClickRemove(id)}>
            <Image src={"/trashicon.svg"} 
            width={20} height={20} /></button>
        </div>
    );
}


이제 우리는 재미있는 부분에 도달하고 실제로 작은 사이트의 스타일을 지정할 것입니다.


페이지/index.js



import TodoList from "../components/TodoList";
import styles from "./index.module.scss";

export default function IndexPage(){
    return (
        <div className={styles.container}>
            <h1>Todo List</h1>
            <TodoList />
        </div>
    );
}




페이지/index.module.scss



.container {
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);

  margin-top: 10em;

  h1 {
    color: white;
    margin-bottom: 2em;
  }
}




구성 요소/TodoList.js



import {useState} from "react";
import TodoItem from "./TodoItem";
import styles from "./todolist.module.scss";

export default function TodoList() {
    const [todos, setTodos] = useState([]);
    const [currentInput, setCurrentInput] = useState("");

    const handleClickAdd = (e) => {
        e.preventDefault();

        if (currentInput === "") {
            //input validation
            return;
        }

        const newTodo = {
            id: currentInput + Math.random().toString(),
            content: currentInput
        }

        let _todos = todos;
        _todos.push(newTodo);

        setTodos(_todos)
        setCurrentInput("")
    }

    const handleClickRemove = (id) => {
        const filteredTodos = todos.filter(todo => todo.id !== id);
        setTodos(filteredTodos);
    }

    return (
        <div className={styles.container}>
            <form onSubmit={(e) => handleClickAdd(e)}>
                <input onChange={(e) =>
                    setCurrentInput(e.target.value)} 
                    value={currentInput}
                    placeholder={"todo..."}/>
                <button type={"submit"}>add</button>
            </form>
            <div className={styles.todos}>
                {
                    todos.map(todo => {
                        return (
                            <TodoItem title={todo.content}
                                      id={todo.id}
                                      handleClickRemove
                                      ={handleClickRemove}/>
                        );
                    })
                }
            </div>
        </div>
    );
}




구성 요소/TodoList.module.scss



.container {
  display: flex;
  flex-direction: column;
  row-gap: 2em;

  form {
    display: flex;
    column-gap: 2em;

    input {
      background: #2C2E33;
      border: none;
      height: 40px;
      width: 250px;
      border-radius: 10px;
      color: white;
      padding-left: 1em;
    }

    input:focus {
      outline: 2px solid #47428E;
    }

    button {
      width: 100px;
      border: none;
      background: #47428E;
      border-radius: 10px;
      color: white;
    }

    button:hover {
      transition: 0.3s;
      background: #3f3a7c;
    }
  }

  .todos {
    display: flex;
    flex-direction: column;
    row-gap: 1em;
  }
}




구성 요소/TodoItem.js



import {useState} from "react";
import Image from "next/image";
import styles from "./todoitem.module.scss";

export default function TodoItem({title, id, handleClickRemove}) {
    const [done, setIsDone] = useState(false);

    const markDone = () => {
        setIsDone(!done);
    }

    return (
        <div onClick={markDone} 
            className={done? styles.container: 
            styles.doneContainer}
            <h2>{title}</h2>
            <button onClick={() => handleClickRemove(id)}>
            <Image src={"/trashicon.svg"} 
            width={20} height={20}/>
            </button>
        </div>
    );
}




구성 요소/todoitem.module.scss



.container {
  background: #2C2E33;
  border-radius: 10px;
  display: flex;
  justify-content: space-between;
  padding: 0.5em 1em 0.5em 1em;
  align-items: center;

  h2 {
    color: white;
    font-size: 20px;
    font-weight: lighter;
  }

  button {
    background: #ce6767;
    height: 40px;
    width: 40px;
    border: none;
    border-radius: 10px;
  }
}

.doneContainer {
  background: #535761;
  border-radius: 10px;
  display: flex;
  justify-content: space-between;
  padding: 0.5em 1em 0.5em 1em;
  align-items: center;

  h2 {
    color: #2b2d31;
    text-decoration: line-through;
    font-size: 20px;
    font-weight: lighter;
  }

  button {
    display:none;
  }
}


오늘은 여기까지입니다. 이 작은 앱을 재미있게 만드셨기를 바랍니다. 당신이 그것을 즐겼다면 좋아요를 남겨주세요.

좋은 웹페이지 즐겨찾기