자바스크립트로 Todolist 구현하기 (feat.API)

개미·2023년 3월 8일
0
post-thumbnail

📌 자바스크립트로 Todolist 구현하기 + API 사용하기

이 프로젝트는 사실 1년 전 개발한 것이다. 하지만, 이번에 여러 기능을 더 덧붙여 완성하였다. 자바스크립트 입문에 아주 좋은 프로젝트라고 생각한다. 프로젝트에 필수 요소인 CRUD가 포함되어 있기 때문이다. 그리고 자신이 넣고 싶은 기능을 조금씩 넣으면 나만의 투두리스트를 만들 수 있다.

웹사이트

https://todolistwithquote.netlify.app/

구현한 기능

  1. 입력란에 할 일을 입력하고 ➕ 버튼을 클릭하거나, 엔터를 누르면 리스트에 할 일이 들어간다.
  2. 🎤 버튼을 누르고 원하는 할 일을 말하면, 음성을 인식하여 말한 내용이 입력란에 입력된다.
  3. 할 일 리스트에서 ✖ 버튼을 누르면, 해당 할 일이 사라진다.
  4. 할 일 리스트에서 🖍 버튼을 누르면, 수정할 수 있도록 프롬프트가 띄워진다.
  5. 새로고침을 할 때마다, 명언들이 랜덤으로 나온다.

📄 HTML

전체 코드

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link
    rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css"
    />
    <link rel="stylesheet" href="style.css">
    <script src="https://kit.fontawesome.com/d6024b91ee.js" crossorigin="anonymous"></script>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
    <title>Todo-list with Quote</title>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js" integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V" crossorigin="anonymous"></script>
    <div id="todolist">
        <div class="main_title">
            <h1>
                <i class="fas fa-clipboard-list"></i>
                Todo-list with Quote
            </h1>
        </div>
        <!--<div class="subtitle">Just write what you have to do!</div>-->
        <div class="quote_text"></div>
        <div class="quote_author"></div>

        <div class="input_section">
            <form onsubmit="return false;">
                <div class = "buttonininput">
                    <input type = "text" class="item" autofocus="true">
                    <button type="button" class="btn-secondary voice_recog"><i class="fa-solid fa-microphone"></i></button>
                </div>
                <div>
                    <button type="button" class="input_button"><i class="fa-solid fa-square-plus"></i></button>
                </div>
            </form>
            
        </div>
        <div class="item_list"></div>
    </div>
    <script src="main.js"></script>
</body>
</html>

1) 아이콘 사용

head에

<link rel="stylesheet" href="[https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css](https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css)"/>

그리고 쓸 부분에 <i class=””></i>해당 웹사이트에서 원하는 아이콘을 골라 복붙을 하면 된다.

2) 주석 처리

주석 처리는 ‘<!— —>

3) <script scr="">의 위치

body의 마지막 부분이 가장 좋다.

브라우저 동작 방식에 의해 중간에 위치하면

  1. HTML을 읽는 과정에 스크립트를 만나면 중단 시점이 생기고 그만큼 Display에 표시되는 것이 지연된다.
  2. DOM 트리가 생성되기전에 자바스크립트가 생성되지도 않은 DOM의 조작을 시도할 수 있다.

이러한 문제들이 생길 수 있다.

최하단에 놓을 수 없을 때는,

<script async src="script.js">

<script defer src="script.js">

둘 다 script 태그를 만나도 html parsing이 중단되지 않는다.

4) 엔터 시 새로고침 방지

<form>

📄 CSS

전체코드

html, body {
  caret-color: #8843d6;
  background: linear-gradient(90deg, #4a138c, #a977e2);
  height: 97%;
  /*background: linear-gradient(90deg, #4a138c 33.333333%, #2d2d2d 33.333333%), linear-gradient(90deg, rgb(47, 47, 47) 66.666666%, #4a138c 66.666666%);*/
}

#todolist {
  text-align: left;
  max-width: 600px;
  margin: 20px auto;
  background-color: #313131;
  height: 100%;
  border-radius: 20px;
}

@font-face {
  font-family: NanumSquareRoundR;
  src: local("NanumSquareRoundR"),
  local("NanumSquareRoundR"),
  url(NanumSquareRoundR.woff2),
  url(NanumSquareRoundR.woff),
  url(NanumSquareRoundR.eot),
  url(NanumSquareRoundR.ttf);
  font-display: swap;
  font-weight: bold;
}

.main_title {
  margin: 0px;
  padding-top: 10px;
  padding-bottom: 0px;
  text-align: center;
  color: white;
  font-family: Impact, Haettenschweiler, 'Arial Narrow Bold', sans-serif;
  font-weight: lighter;
}

.subtitle {
    color: white;
    text-align: center;
    font-style:italic;
    font-size:10px;
}

.quote_text {
    text-align: center;
    font-size: 12px;
    color:rgb(255, 255, 255);
    font-style: italic;
    padding-top: 0px;
    margin:0px 20px;
    font-weight: 1000;
}
.quote_author {
    text-align:right;
    font-size: 12px;
    margin-right: 6rem;
    font-style: italic;
    color:rgb(255, 255, 255);


}


.input_section {
  margin: 0px 100px 5px 140px;
  align-items: center;
  display: flex;
}

.input_section form {
  align-items: center;
  display: flex;
}

.item {
  width: 300px;
  height: 30px;
  border-radius: 15px;
  border: 1px solid #fbe7c6;
  padding: 0px 30px;
}

*:focus {
  outline: none;
}

.input_button {
  background-color: transparent;
  font-size: 30px;
  line-height: 60px;
  margin-left: 5px;
  color: white;
  border: 0;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

ul li {
  cursor: pointer;
  position: relative;
  padding: 12px 8px 12px 100px;
  background: #eee;
  font-size: 18px;
  transition: 0.3s;
  margin: 0px 100px;
  font-size: 15px;
}

ul li:hover {
  background: #ddd;
}

.close {
  position: absolute;
  right: 10px;
  top: 0;
  align-items: center;
  text-align: center;
  margin: 8px 60px;
  padding: 4px 8px;
  border: none;
  background: rgba(255, 255, 255, 0);
}
.close:hover {
  background: #4a138c;
  border-radius: 100%;
  color: white;
}
.cor {
  position: absolute;
  right: 5px;
  top: 0px;
  align-items: center;
  text-align: center;
  margin: 8px 30px;
  padding: 8px 8px;
}
.cor:hover {
  background: #4a138c;
  border-radius: 100%;
  color: white;
}

ul li.checked {
  background: #ddd;
  color: #8b879b;
  text-decoration: line-through;
  font-size: 15px;

}

ul li.checked::before {
  content: "";
  position: absolute;
  border-color: #272341;
  border-style: solid;
  border-width: 0 2px 2px 0;
  top: 13px;
  left: 80px;
  transform: rotate(45deg);
  height: 15px;
  width: 7px;
}

.buttonininput {
  position: relative;
}

.item {
  border: none;
  padding: 0 15px;
  height: 40px;
}
.voice_recog {
  position: absolute;
  top: 15%;
  right: 5px;
  border-radius: 50%;
  background-color: lightgray;
  border: none;
  width: 30px;
  height: 30px;
}



@media screen and (max-width: 768px) {
  body {
    margin: 0;
  }
  #todolist {
    max-width: 100%;
  }

  .input_section form {
    margin: 0px 30px 5px 30px;
    align-items: center;
    display: flex;
    justify-content: space-between;
  }

  .item {
    width: 200px;
    height: 30px;
    border-radius: 15px;
    border: 1px solid #fbe7c6;
    padding: 0px 30px;
  }

  .close {
    position: absolute;
    align-items: center;
    text-align: center;
    margin: 8px 15px;
    padding: 4px 8px;
    border: none;
    background: rgba(255, 255, 255, 0);
  }
}

1) 배경 그라데이션

background: linear-gradient(90deg, #4a138c, #a977e2);

2) height을 조정하고 싶을 때

html, body {
  height: 97%;
}

원하는 height을 주고싶은 element의 부모 요소에 원하는 height을 넣어주면 된다.

3) 클릭시 테두리 생기는 것 없애기

*:focus {
    outline: none;
  }

4) 화살표 트릭으로 만들기

ul li.checked::before {
    content: "";
    position: absolute;
    border-color: #272341;
    border-style: solid;
    border-width: 0 2px 2px 0;
    top: 13px;
    left: 80px;
    transform: rotate(45deg);
    height: 15px;
    width: 7px;
	}

5) 반응형 만들기

@media screen and (max-width: 768px){...}

태블릿 디바이스 (가로 해상도가 768px 보다 작은 화면에 적용)

6) position 속성

  • static: 기본값, 다른 태그와의 관계에 의해 자동으로 배치되며 위치를 임의로 설정해 줄 수 없습니다.
  • absolute: 절대 좌표와 함께 위치를 지정해 줄 수 있습니다.
  • relative원래 있던 위치를 기준으로 좌표를 지정합니다.
  • fixed: 스크롤과 상관없이 항상 문서 최 좌측상단을 기준으로 좌표를 고정합니다.
  • inherit: 부모 태그의 속성값을 상속받습니다.

relative인 컨테이너 내부에 absolute인 객체가 있으면 절대 좌표를 계산할 때, relative컨테이너를 기준점으로 잡게 됩니다. (없다면 전체 문서가 기준)

7) 렌더링

@font-face {
  font-family: NanumSquareRoundR;
  src: local("NanumSquareRoundR"),
  local("NanumSquareRoundR"),
  url(NanumSquareRoundR.woff2),
  url(NanumSquareRoundR.woff),
  url(NanumSquareRoundR.eot),
  url(NanumSquareRoundR.ttf);
  font-display: swap;
  font-weight: bold;
}

브라우저 렌더링을 보고, 폰트 용량을 줄이고자 했다. 하지만 나의 경우에는 다른 폰트라서 적용이 안되는 듯하다.

📄 Javascript

전체 코드

'use strict';

window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

var itemList = [];
var inputButton = document.querySelector(".input_button");
inputButton.addEventListener("click", addItem);

document.querySelector(".item").addEventListener('keypress',function (e){
  if(e.key === 'Enter'){
      addItem();
  }
})

function addItem() {
  var item = document.querySelector(".item").value;
  if (item != "") {
      itemList.push(item);
      document.querySelector(".item").value = "";
      document.querySelector(".item").focus();
  }

  showList();
}


function showList() {
  var list = "<ul>"
  for (var i = 0; i <itemList.length; i++) {
      list += "<li>" + itemList[i] + "<span class='close' id=" + i + ">" + "\u00D7" + "</span><i class='fa-solid fa-pen cor'  id=" + i + ">" + "</i></li>";
  }
  list += "</ul>";
  document.querySelector(".item_list").innerHTML = list;


  var devareButtons = document.querySelectorAll(".close");
  var corButtons = document.querySelectorAll(".cor");
  for (var i = 0; i < devareButtons.length; i++) {
      devareButtons[i].addEventListener("click", devareItem);
      corButtons[i].addEventListener("click", correctItem);
  }
}

function devareItem() {
  var id = this.getAttribute("id");
  itemList.splice(id, 1);
  showList();
}


function correctItem() {
  let text = prompt("수정할 내용을 입력하세요.");
  if (text == null || text == "") {
      return;
  }
  var id = this.getAttribute("id");
  itemList[id] = text;
  showList();
}

var checkList = document.querySelector('.item_list');
checkList.addEventListener('click', event => {
if (event.target.tagName === 'LI') {
  event.target.classList.toggle('checked');
}
});

var quoteText = document.querySelector(".quote_text")
var quoteAuthor = document.querySelector(".quote_author")

function getQuote(){
  function randomItem(a){
      return a[Math.floor(Math.random()*a.length)];
  }

  fetch("https://type.fit/api/quotes")
      .then(function(response) {
          return response.json();
      })
      .then(function(data) {
          var random = randomItem(data);
          var author = random.author;
          var text = random.text;
          quoteText.innerText = `${text}`;
          quoteAuthor.innerText = `- ${author} -`;
      });
}

getQuote();
document.querySelector(".quote_text").innerHTML = quoteText.innerText;
document.querySelector(".quote_author").innerHTML = quoteAuthor.innerText;


function init(){
  getQuote();
}

let recognition = new SpeechRecognition();
recognition.interimResults = true; // 중간 결과를 반환할지 여부
recognition.lang = 'ko-KR';

recognition.onresult = function(event) {
  console.log(event.results)
  var text = event.results[0][0].transcript;
  console.log(text);
  document.querySelector(".item").value = text;
  document.querySelector(".item").focus();
}

recognition.onspeechend = () => {
  recognition.stop();
}

document.querySelector(".voice_recog").addEventListener('click', function(){
  recognition.start();
});

1) 엄격모드

'use strict';

strict mode는 자바스크립트 언어의 문법을 좀 더 엄격히 적용하여 오류를 발생시킬 가능성이 높거나 자바스크립트 엔진의 최적화 작업에 문제를 일으킬 수 있는 코드에 대해 명시적인 에러를 발생시킨다. (콘솔에 에러를 확인할 수 있다.)

2) 이벤트

이벤트를 등록하는 두가지 방식

  • 이벤트 핸들러 프로퍼티 방식
const $button = document.querySelector('button');

$button.onclick = function(){
	console.log('button click');
};
  • addEventListner 메서드 방식 ⬅️ 이 방법을 사용함.
const $button = document.querySelector('button');

$button.addEventListner('click',function(){
	console.log('button click');
});

3) addItem()

function addItem() {
    var item = document.querySelector(".item").value;
    if (item != null) {
        itemList.push(item);
        document.querySelector(".item").value = "";
        document.querySelector(".item").focus();
    }

    showList();
}

할 일을 추가해주는 함수

document.querySelector(".item").value = ""; ➡️ 입력창 비워줌

document.querySelector(".item").focus(); ➡️ 커서 깜빡임

4) showList()

function showList() {
    var list = "<ul>"
    for (var i = 0; i <itemList.length; i++) {
        list += "<li>" + itemList[i] + "<span class='close' id=" + i + ">" + "\u00D7" + "</span></li>";
    }
    list += "</ul>";
    document.querySelector(".item_list").innerHTML = list;

    var devareButtons = document.querySelectorAll(".close");
    for (var i = 0; i < devareButtons.length; i++) {
        devareButtons[i].addEventListener("click", devareItem);
    }
}

할 일 리스트를 보여주는 함수

"\u00D7" ➡️ ‘x’버튼 클래스는 close

document.querySelector(".item_list").innerHTML = list; ➡️ class가 item_list인 곳에 HTML 형식으로 넣어줌. 즉 list는 HTML형식이다.

devareButtons[i].addEventListener("click", devareItem); ➡️ 각 close 버튼에 이벤트를 넣어준다.

5) devareItem()

function devareItem() {
    var id = this.getAttribute("id");
    itemList.splice(id, 1);
    showList();
}

해당 할 일을 삭제하는 함수
itemList.splice(id, 1); ➡️ id번 인덱스에서 1개 제거

6) getQuote()

var quoteText = document.querySelector(".quote_text")
var quoteAuthor = document.querySelector(".quote_author")

function getQuote(){
    function randomItem(a){
        return a[Math.floor(Math.random()*a.length)];
    }

    fetch("https://type.fit/api/quotes")
        .then(function(response) {
            return response.json();
        })
        .then(function(data) {
            var random = randomItem(data);
            var author = random.author;
            var text = random.text;
            quoteText.innerText = `${text}`;
            quoteAuthor.innerText = `- ${author} -`;
        });
}

getQuote();
document.querySelector(".quote_text").innerHTML = quoteText.innerText;
document.querySelector(".quote_author").innerHTML = quoteAuthor.innerText;

random으로 명언을 뽑는 함수

fetch란 무엇인가?

promise 형태로 리턴하며 비동기적 동작 가능성이 높다.
성공적으로 실행되면 response 객체를 리턴한다.

성공시 then(function(response){...})
실패시 catch(function(reason){...})
안에 function은 콜백함수이다.

7) correctItem()

 function correctItem() {
   let text = prompt("수정할 내용을 입력하세요.");
   if (text == null || text == "") {
       return;
   }
   var id = this.getAttribute("id");
   itemList[id] = text;
   showList();
}

해당 할 일을 수정해주는 함수
입력값이 없으면 그대로 유지하도록 하였다.

8) WebSpeechAPI

let recognition = new SpeechRecognition();
recognition.interimResults = true; // 중간 결과를 반환할지 여부
recognition.lang = 'ko-KR';

recognition.onresult = function(event) {
  console.log(event.results)
  var text = event.results[0][0].transcript;
  console.log(text);
  document.querySelector(".item").value = text;
  document.querySelector(".item").focus();
}

recognition.onspeechend = () => {
  recognition.stop();
}

document.querySelector(".voice_recog").addEventListener('click', function(){
  recognition.start();
});

MDN 웹 문서에서 Web Speech API의 정보를 확인 할 수 있다.
SpeechRecognition 객체를 사용하여 음성 인식 기능을 구현하였다. SpeechRecognition에는 여러 프로퍼티, 메서드, 이벤트 핸들러가 있다.

Property
SpeechRecognition.lang = 'ko-KR' : 언어를 한국어로 설정
SpeechRecognition.continuous : 인식된 음성을 쭉 이어서 받을지(true), 하나씩만 받을지(false: default) 결정
SpeechRecognition.iterimResults: 실시간으로 음성 인식한 결과를 화면에 표시할지(true), 다 끝나면 완료된 결과만 보이게 할지(false)

Method
SpeechRecognition.abort(): 현재 들어오는 음성 데이터를 듣고 있는 speech recognition 서비스를 멈춘다. SpeechRecognitionResult 객체를 리턴하지 않는다.
SpeechRecognition.start(): 음성 인식 시작
SpeechRecognition.stop(): 음성 인식 멈춤. SpeechRecognitionResult 객체 리턴.

Event Handler
onstart: 음성 데이터를 듣기 시작할 때 실행되는 이벤트 핸들러
onend: 음성 인식이 끊기면 실행
onerror: 에러 발생시 실행
onresult: 음성 인식 서비스가 result를 리턴할 때 실행

그렇다면 event.results는 무엇일까? 이는 여러개의 SpeechRecognitionResult 객체를 가지고 있는 SpeechRecognitionResultList 라는 객체이다. 이 리스트에 result 객체를 계속 쌓아둔 것이다.

더 나아가, 왜 event.result[0][0] 일까? SpeechRecognitionResult 객체는 여러개의 SpeechRecognitionAlternative 객체로 이뤄져있기 때문에 event.result[0][0] 2차원으로 접근해야 SpeechRecognitionAlternative.transcript 으로 인식된 단어의 string 값을 리턴받을 수 있다.

배포

netlify에서 github를 연동하여 쉽게 배포할 수 있다.

참고

https://velog.io/@janeljs/making-a-todolist
https://2donny-world.tistory.com/10
https://www.youtube.com/playlist?list=PLuHgQVnccGMBVQ4ZcIRmcOeu8uktUAbxI

profile
개발자

0개의 댓글