[JS] Vanila JavaScript로 TODO List 만들기

Kim yeonhee·2025년 1월 3일
0

JavaScript

목록 보기
5/13

바닐라JS를 공부하면서 작은 프로젝트를 하나 개발해보려고 한다.
역시 가장 무난한건 TODO List일 것이다.


이런 느낌으로 간단하게 구현해보려고 한다.
개발 환경은 Visual Studio Code를 사용했고, HTML, CSS, JavaScript로 개발할 예정이다.

💡 기능 구현 목록

  • OpenWeather에서 날씨 API를 받아와서 날씨를 상단에 띄우기
  • 현재 시간 띄우기
  • 인사말 문구를 랜덤으로 띄우고 사용자의 이름을 localStorage에 저장해 띄우기
  • 할 일 입력 폼에 할일을 입력하면 자동으로 할일이 등록
  • 할 일 삭제 버튼을 누르면 할 일이 삭제 된다.

구현 화면

코드 GitHub 주소

index.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="../css/style.css">
    <title>TODO List</title>
</head>
<body>
    <div id="container">
        <!-- Contents -->
        <div id="contents" class="hidden">
            <p id="weather" class="small-text">날씨</p>
            <h1 id="clock">00:00:00</h1>
            <p id="greeting"></p>
            <form onsubmit="onTodoSubmit()">
                <input type="text" id="input-task" placeholder="Enter tasks..." pattern="[A-Za-z\s]+$" autocomplete="off" autofocus>
            </form>
            <ul id="task-list"></ul>
        </div>
        <!-- Login -->
        <div id="login_form">
            <h1>Welcome TODO App</h1>
            <form onsubmit="onLoginSubmit(event)">
                <input type="text" id="username" placeholder="Enter your nickname." pattern="[A-Za-z\s]+$" autocomplete="off" autofocus>
            </form>
        </div>
    </div>
    <script src="../scripts/util.js" type="module"></script>
    <script src="../scripts/app.js"></script>
    <script src="../scripts/weather.js" type="module"></script>
</body>
</html>

클래스를 지정해서 숨기고 싶은 컨텐츠에 hidden 클래스를 지정했다.
css에서 hidden클래스의 경우 display: none으로 설정해 해당 컨텐츠를 숨겼다.


📑 사용자 이름, 할 일 입력

첫 화면에서 사용자 이름을 영어와 공백만 입력 가능하도록 input에 정규 표현식을 사용했다.
영어와 공백만 입력이 가능하도록 정규표현식을 사용했다. ([A-Za-z\s]+$)

  • [A-Za-z] : 대소문자 알파벳을 의미한다.
  • \s : 공백 문자(스페이스, 탭, 줄바꿈 등)을 의미한다.
  • + : 앞의 문자 클래스 [A-Za-z\s]가 하나 이상 나올 수 있다는 것을 의미한다.
  • $ : 문자열의 끝을 의미한다.

📑 API 키 관리 방법

날씨는 현재 사용자의 위치 정보를 받아와서 OpenWeather API에서 API키를 가져올 수 있도록 구현했다.
코드는 GitHub에 업로드하기 때문에 보안상 따로 관리가 필요하다고 생각했다.
GitHub에 커밋하지 않을 스크립트를 따로 모듈화해서 관리하기 위해 util.js를 생성했다.
util.js에서 export해서 weather.jsimport해서 키를 가져다 쓰는 방식이다.


style.css

@charset "utf-8";
@import url('https://fonts.googleapis.com/css2?family=Jersey+15&family=Josefin+Sans:ital,wght@0,100..700;1,100..700&display=swap');

*{
    font-family: Josefin Sans;
}
#container{
    width: 600px;
    margin: auto;
}
#contents{
    text-align: center;
}
#clock{
    font-size: 70px;
    color: orange;
}
.small-text{
    font-size: small;
    color: silver;
    margin-top: 30px;
}
.hidden{
    display: none;
}
/* contents : 랜덤 인사 문구 */
#greeting{
    font-weight: bold;
    font-size: 30px;
    margin-top: -25px;
    color: rgb(88, 58, 0);
}
/* 로그인 시 이름 입력 폼 */
#login_form{
    text-align: center;
}

#username{
    border: 2px solid gray;
    width: 100%;
    height: 100px;
    font-size: 30px;
    text-align: center;
}
/* 할 일 입력폼 */
input:focus{
    outline: none;
}
#input-task{
    border: 2px dashed rgba(128, 128, 128, 0.26);
    padding: 15px;
    text-align: center;
    font-size: 20px;
    width: 100%;
}
#task-list > li{
    padding: 20px;
    text-align: left;
    font-size: 20px;
    display: flex;
    justify-content : space-between;
    height: 30px;
}
.delBtn{
    all: unset;
    transition: all 0.3s;
    transform-origin: center;
}
.delBtn:hover{
    transform: scale(1.2);
}

app.js

let contents = document.querySelector("#contents");
let login_form = document.querySelector("#login_form");
let greeting = document.querySelector("#greeting");
const clock = document.querySelector('#clock');
let task_array = [];

const HIDDEN_CLASSNAME = "hidden";
const USER_NAME = "userName"
const TASKS = "tasks";

const greeting_arr = [
    "Hello",
    "Good day",
    "Hey there",
    "How are things",
    "What's happening",
    "Yo",
    "What's the story"
];

// 프로그램이 시작되자마자 유효성 검사를 실시 한다.
// 유저 네임이 있는 경우
if(localStorage.getItem(USER_NAME)){
    setClock();
    setUserName(localStorage.getItem(USER_NAME));
    const savedTasks = localStorage.getItem(TASKS);   // 저장되어있는 할 일 목록
    // console.log(`savedTasks : ${savedTasks}`);
    if(savedTasks){
        task_array = JSON.parse(savedTasks);    // JSON.parse() : JSON을 객체로 변경한다. 
        displayTasks(task_array);
    } 
}

// 현재 시간
function setClock(){
    const date = new Date();
    const hour = String(date.getHours()).padStart(2, '0');
    const minute = String(date.getMinutes()).padStart(2, '0');
    const seconds = String(date.getSeconds()).padStart(2, '0');
    clock.innerText = `${hour}:${minute}:${seconds}`;    
}
setInterval(setClock, 1000);


// 로그인 Submit
function onLoginSubmit(event){
    let userName = document.querySelector("#username").value;
    localStorage.setItem(USER_NAME, userName);
    event.preventDefault();
    setUserName(userName);
}

// 'userName'이 있는 경우 로그인 화면이 아닌 컨텐츠 화면을 바로 보여준다.
function setUserName(userName){
    login_form.classList.add(HIDDEN_CLASSNAME);
    contents.classList.remove(HIDDEN_CLASSNAME);
    greeting.innerHTML = `${randomGreeting()} ${userName}!`;
}

// 페이지가 로드될 때마다 출력되는 랜덤 인삿말
function randomGreeting(){
    let randomIndex = Math.floor(Math.random() * greeting_arr.length);
    return greeting_arr[randomIndex];
}

// Todo 입력 Submit
function onTodoSubmit(){
    let task = document.querySelector('#input-task').value;
    task_array.push(task);
    localStorage.setItem(TASKS, JSON.stringify(task_array));
    displayTasks(task_array);
}

// 할 일 목록을 화면에 출력한다.
function displayTasks(task_array){
    let taskList = document.querySelector('#task-list');
    taskList.innerHTML = '';    // 기존 목록 비우기
    for(let i = 0; i < task_array.length; i++){
        let li = document.createElement('li');
        let delBtn = document.createElement('button');
        li.setAttribute('data-index', i);
        delBtn.innerHTML = '❌';
        delBtn.setAttribute('class', 'delBtn');
        delBtn.setAttribute('type', 'button');
        li.innerHTML = `<span>${task_array[i]}</span>`;
        li.appendChild(delBtn);
        taskList.append(li);

        // 삭제버튼 눌렀을 때
        delBtn.addEventListener('click', function(){
            // console.log(`클릭한 항목의 인덱스 : ${li.getAttribute('data-index')}`);
            let index = li.getAttribute('data-index');
            let tasks = JSON.parse(localStorage.getItem(TASKS));
            tasks.splice(index, 1);
            localStorage.setItem(TASKS, JSON.stringify(tasks));
            taskList.removeChild(li);
        });
    }
}

📑 사용자 이름 저장

사용자의 이름을 입력하는 경우 localStorageuserName으로 저장한다.
저장 하면 페이지를 껐다가 켜도 localStorageuserName을 삭제하지 않는 한 사용자의 정보가 저장된다.
화면을 켰을 때 userName이 있는지 검사하고, 만약 있다면 현재 시간 정보를 출력한다.
로그인 화면은 hidden클래스를 추가하여 숨기고, 컨텐츠 화면은 hidden클래스를 삭제하여 화면에 띄운다.


📑 할 일 목록 추가

할 일을 입력하면 해당 valuetask_array 배열에 저장한다.
JSON.stringify(task_array)배열을 문자열로 변환하는 메서드이다.
localStorage는 문자열만 저장할 수 있기 때문에 배열을 저장하기 위해 JSON.stringify()를 사용해서 문자열로 변환했다.

for문을 사용해서 task_array에 저장된 할 일 목록을 순회하며 할 일 갯수 만큼 리스트 항목 요소와 삭제 버튼을 생성한다.

리스트 항목 요소에는 data-index라는 커스텀 속성을 추가해서 삭제할 때 어떤 항목을 삭제할 지 알아내기 위해서 해당 항목의 인덱스를 저장한다.


📑 할 일 목록 삭제

splice()는 배열에서 요소를 제거하는 메소드로 task.splice(index, 1)를 사용해서 해당 인덱스의 항목을 배열에서 삭제한다.

localStorage.setItem(TASKS, JSON.stringify(tasks)); 는 수정된 할 일 목록을 다시 로컬 저장소에 저장한다. 삭제된 항목을 제외한 새로운 목록을 문자열로 반환하여 저장한다.


weather.js

import { WEATHER_API_KEY } from './util.js';

function onGeoSuccess(position){
    let latitude = position.coords.latitude;    // 위도
    let longitude = position.coords.longitude;  // 경도
    const url = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${WEATHER_API_KEY}&units=metric`;
    fetch(url).then(response => response.json()).then(data => {
        // console.log(data.name, data.weather[0].main);
        const weather_container = document.querySelector('#weather');
        const name = data.name;
        const weather = data.weather[0].main;
        weather_container.innerHTML = `${name} is ${weather} !`;
    }); 

}
// 사용자의 현재 위치를 요청한다.
navigator.geolocation.getCurrentPosition(onGeoSuccess);

navigator.geolocation.getCurrentPosition() 함수를 이용해서 현재 사용자의 위치 정보를 받아온다.


페이지를 실행하면 권한 요청 알람이 뜨는데 허용하는 경우에 onGeoSuccess()함수가 실행된다.
API요청을 위한 url을 생성하고, fetch()함수를 이용해서 위에서 생성한 url에 HTTP 요청을 보내고 반환되는 응답을 비동기 처리한다.


util.js

export const WEATHER_API_KEY = "apiKey";

따로 발급받은 키를 관리하기 위한 util 스크립트를 만들어서 키를 관리한다.



마치며..

늘 이론 공부보다 실습하는 것이 더 중요하다고 느낀다. 다 알고 있었던 것들이지만 직접 구현해보니 애로 사항이 은근히 많았다. 바닐라 자바스크립트가 모든 프레임워크, 라이브러리의 근간이 되니 간단한 프로젝트라도 배운 것이 많아서 재미있었다!!

0개의 댓글