바닐라JS를 공부하면서 작은 프로젝트를 하나 개발해보려고 한다.
역시 가장 무난한건 TODO List일 것이다.
이런 느낌으로 간단하게 구현해보려고 한다.
개발 환경은 Visual Studio Code
를 사용했고, HTML
, CSS
, JavaScript
로 개발할 예정이다.
- OpenWeather에서 날씨 API를 받아와서 날씨를 상단에 띄우기
- 현재 시간 띄우기
- 인사말 문구를 랜덤으로 띄우고 사용자의 이름을
localStorage
에 저장해 띄우기- 할 일 입력 폼에 할일을 입력하면 자동으로 할일이 등록
- 할 일 삭제 버튼을 누르면 할 일이 삭제 된다.
<!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]
가 하나 이상 나올 수 있다는 것을 의미한다.$
: 문자열의 끝을 의미한다.
날씨는 현재 사용자의 위치 정보를 받아와서 OpenWeather API에서 API키를 가져올 수 있도록 구현했다.
코드는 GitHub
에 업로드하기 때문에 보안상 따로 관리가 필요하다고 생각했다.
GitHub
에 커밋하지 않을 스크립트를 따로 모듈화해서 관리하기 위해 util.js
를 생성했다.
util.js
에서 export
해서 weather.js
에 import
해서 키를 가져다 쓰는 방식이다.
@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);
}
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);
});
}
}
사용자의 이름을 입력하는 경우 localStorage
에 userName
으로 저장한다.
저장 하면 페이지를 껐다가 켜도 localStorage
의 userName
을 삭제하지 않는 한 사용자의 정보가 저장된다.
화면을 켰을 때 userName
이 있는지 검사하고, 만약 있다면 현재 시간 정보를 출력한다.
로그인 화면은 hidden
클래스를 추가하여 숨기고, 컨텐츠 화면은 hidden
클래스를 삭제하여 화면에 띄운다.
할 일을 입력하면 해당 value
를 task_array
배열에 저장한다.
JSON.stringify(task_array)
는 배열을 문자열로 변환하는 메서드이다.
localStorage
는 문자열만 저장할 수 있기 때문에 배열을 저장하기 위해 JSON.stringify()
를 사용해서 문자열로 변환했다.
for
문을 사용해서 task_array
에 저장된 할 일 목록을 순회하며 할 일 갯수 만큼 리스트 항목 요소와 삭제 버튼을 생성한다.
리스트 항목 요소에는 data-index
라는 커스텀 속성을 추가해서 삭제할 때 어떤 항목을 삭제할 지 알아내기 위해서 해당 항목의 인덱스를 저장한다.
splice()
는 배열에서 요소를 제거하는 메소드로 task.splice(index, 1)
를 사용해서 해당 인덱스의 항목을 배열에서 삭제한다.
localStorage.setItem(TASKS, JSON.stringify(tasks));
는 수정된 할 일 목록을 다시 로컬 저장소에 저장한다. 삭제된 항목을 제외한 새로운 목록을 문자열로 반환하여 저장한다.
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 요청을 보내고 반환되는 응답을 비동기 처리한다.
export const WEATHER_API_KEY = "apiKey";
따로 발급받은 키를 관리하기 위한 util
스크립트를 만들어서 키를 관리한다.
늘 이론 공부보다 실습하는 것이 더 중요하다고 느낀다. 다 알고 있었던 것들이지만 직접 구현해보니 애로 사항이 은근히 많았다. 바닐라 자바스크립트가 모든 프레임워크, 라이브러리의 근간이 되니 간단한 프로젝트라도 배운 것이 많아서 재미있었다!!