nomadcoder | Todo App 만들기 프로젝트 - 피드백과 리팩토링 🙇🏻♀️

프로젝트 배포까지 깃허브에서 간단하게 끝낸 후 그룹 스터디, 토이 프로젝트 1 팀원들 그리고 강사님께도 공유 드려서 피드백을 부탁드렸다 ! 역시나 아직 배울게 많고 배웠어도 적용이 안되는 것이 많구나 ,, ㅎ
1. HTML 코드에서 각 스크립트가 서로 데이터나 함수를 공유하는 부분이 없어서 순서대로 호출될 필요가 없어 보입니다. 그래서 defer 처럼 HTML 요소가 준비되면 실행하지만, 동기가 아닌 비동기로 각 스크립트가 호출될 수 있는 async 속성을 사용하는 게 현재 프로젝트에서는 더 좋은 방법입니다.
기존 코드
<script defer src="js/greeting.js"></script>
<script defer src="js/clock.js"></script>
<script defer src="js/quotes.js"></script>
<script defer src="js/background.js"></script>
<script defer src="js/todo.js"></script>
<script defer src="js/weather.js"></script>
피드백 코드
<script async src="js/greeting.js"></script>
<script async src="js/clock.js"></script>
<script async src="js/quotes.js"></script>
<script async src="js/background.js"></script>
<script async src="js/todo.js"></script>
<script async src="js/weather.js"></script>
==> defer와 async의 공통점과 차이점은 뭐지 ? (여태 defer만 알고 있어서 정말 새로운 정보였다)
async는 스크립트가 다운로드되는 즉시 실행되며, 문서 파싱 순서와 상관없이 실행됩니다.defer는 스크립트가 문서의 파싱이 완료된 후, 순서대로 실행됩니다.
async와 defer는 둘 다 <script> 태그에 사용되어, 외부 스크립트를 로드하고 실행하는 방식을 제어하는 속성입니다.
이 두 속성의 공통점과 차이점을 이해하면, 언제 어떤 속성을 사용하는 것이 적합한지 알 수 있습니다.
공통점
1. 비동기 로드: 두 속성 모두 스크립트 파일이 비동기로 로드됩니다. 즉, HTML 문서가 파싱되는 동안 스크립트가 동시에 다운로드됩니다. 이렇게 하면 페이지 로드 성능이 향상될 수 있습니다.
2. 페이지 렌더링 차단 없음: 두 속성 모두 스크립트 다운로드가 진행되는 동안 HTML 파싱을 차단하지 않으므로, 페이지가 빠르게 렌더링될 수 있습니다.
차이점
1. 스크립트 실행 시점
• async:
• 스크립트가 다운로드된 즉시 실행됩니다.
• HTML 문서의 파싱은 계속되지만, 스크립트가 준비되면 즉시 실행되므로 스크립트 실행 시점이 예측 불가능합니다.
• 여러 개의 async 스크립트는 로드되는 순서와 상관없이 먼저 로드된 스크립트가 먼저 실행됩니다.
• defer:
• 스크립트가 다운로드된 후, HTML 문서의 파싱이 완료될 때까지 실행을 지연시킵니다.
• 모든 defer 스크립트는 HTML 파싱이 완료된 후, 원래 문서에 등장한 순서대로 실행됩니다.
• 일반적으로 <head> 안에 스크립트를 배치하는 경우, defer를 사용하면 스크립트가 문서 하단에 위치한 것처럼 동작합니다.
2. 사용 사례
• async:
• 다른 스크립트나 HTML 요소에 의존하지 않는 독립적인 스크립트에 적합합니다.
예를 들어, 광고, 분석 코드, 소셜 미디어 위젯 등과 같이 페이지의 다른 부분과 상호작용하지 않는 스크립트에 사용합니다.
• defer:
• HTML 문서와 상호작용하거나 다른 스크립트에 의존하는 경우에 적합합니다.
예를 들어, DOM 요소와 상호작용하는 스크립트나 다른 스크립트에 의존하는 경우 defer를 사용하는 것이 좋습니다.
2. HTML에서 로딩 요소는 id 속성이 아닌 class 속성으로 중복 이름(loading)을 추가해서 JS에서 제어하는 게 좋겠습니다. id 속성은 고유해야 하기 때문에, 특수한 고유 요소를 지칭하는 용도가 아니면 굳이 사용할 필요가 없습니다.
기존 코드
<div id="weather">
<div id="loadingSpinner"></div>
<span id="geneve">
<h3></h3>
<h3></h3>
</span>
<h2 id="clock_geneve">00:00:00</h2>
<div id="loadingSpinner2"></div>
<span id="seoul">
<h3></h3>
<h3></h3>
</span>
<h2 id="clock">00:00:00</h2>
</div>
피드백 코드
<div id="weather">
<div id="loadingSpinner" class="loadingGeneve"></div>
<span id="geneve">
<h3></h3>
<h3></h3>
</span>
<h2 id="clock_geneve">00:00:00</h2>
<div id="loadingSpinner2" class="loadingSeoul"></div>
<span id="seoul">
<h3></h3>
<h3></h3>
</span>
<h2 id="clock">00:00:00</h2>
</div>
3. document.addEventListener("DOMContentLoaded") 이벤트는 defer 혹은 async 키워드를 사용하는 스크립트에서는 필요치 않아요.
DOM이란?
• DOM은 Document Object Model의 약자로, 웹 페이지의 HTML 문서를 브라우저가 이해할 수 있는 구조로 변환한 것입니다.
DOM 요소(DOM element)는 웹 페이지의 구조를 이루는 개별적인 구성 요소입니다.
간단히 말해, HTML 태그로 정의된 모든 항목이 DOM 요소라고 할 수 있습니다.
• 브라우저는 HTML 문서를 읽고, 이를 트리 형태의 구조로 변환하여 관리합니다. 이 트리 구조의 각 노드(node)가 DOM 요소입니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>This is a paragraph.</p>
<button>Click me</button>
</body>
</html>
이 HTML 문서에서 각각의 태그는 DOM 요소입니다:
• <html>, <head>, <body> 태그: 페이지의 기본 구조를 정의하는 요소들입니다.
• <h1> 태그: “Hello, World!“라는 텍스트를 포함하는 제목 요소입니다.
• <p> 태그: “This is a paragraph.“라는 텍스트를 포함하는 단락 요소입니다.
• <button> 태그: “Click me”라는 버튼 요소입니다.
DOMContentLoaded 이벤트가 필요한 경우: 인라인 스크립트 또는 defer나 async 속성이 없는 스크립트가 <head>에 위치하며 DOM이 완전히 로드된 후에 실행되어야 하는 경우.
DOMContentLoaded 이벤트가 필요하지 않은 경우: defer 속성을 사용한 스크립트, 또는 DOM 조작이 필요하지 않고 독립적으로 실행되는 async 스크립트.
언제 DOMContentLoaded 이벤트를 사용해야 하나요?
• 인라인 스크립트 또는 defer, async를 사용하지 않는 스크립트:
• 스크립트가 <head> 태그에 배치된 경우: defer나 async 속성이 없는 경우, 스크립트는 HTML 문서를 위에서 아래로 읽어나가는 과정에서 즉시 실행됩니다. 이 경우, 스크립트가 실행될 때 DOM 요소가 아직 준비되지 않았을 수 있습니다. 따라서 DOM이 완전히 로드된 후에 코드를 실행하도록 하려면 DOMContentLoaded 이벤트를 사용해야 합니다.
• 특정 시점 이후에 DOM 조작이 필요한 경우: 예를 들어, HTML 요소가 완전히 생성된 후에 그 요소를 조작해야 하는 경우, DOMContentLoaded 이벤트를 통해 DOM이 완전히 준비된 후에 코드를 실행할 수 있습니다.
<head>
<script>
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("example").innerText = "DOM is fully loaded!";
});
</script>
</head>
<body>
<p id="example">Loading...</p>
</body>
이 코드에서는 DOMContentLoaded 이벤트가 발생하기 전에는 #example 요소를 조작할 수 없습니다.
언제 DOMContentLoaded 이벤트를 사용하지 않아도 되나요?
• defer 속성을 사용하는 경우:
• defer 속성이 있는 스크립트는 HTML 파싱이 완료된 후에 순서대로 실행되기 때문에, DOM이 완전히 로드된 후에 스크립트가 실행됩니다. 따라서 DOMContentLoaded 이벤트를 사용할 필요가 없습니다. defer를 사용하면 기본적으로 DOMContentLoaded 이벤트 후에 실행되는 것과 동일한 효과를 얻을 수 있습니다.
<head>
<script defer src="script.js"></script>
</head>
<body>
<p id="example">Loading...</p>
</body>
script.js 파일 내에서 DOMContentLoaded 이벤트를 사용하지 않고도 DOM 조작을 안전하게 할 수 있습니다.
• async 속성을 사용하는 경우:
• async 속성이 있는 스크립트는 다운로드가 완료되는 즉시 실행되며, 이때 DOM이 아직 완전히 로드되지 않았을 수 있습니다. 따라서 DOMContentLoaded와 함께 사용하지 않는다면, DOM 조작이 안전하지 않을 수 있습니다. 하지만 async는 주로 다른 스크립트나 DOM 조작과 독립적인 스크립트에서 사용되므로, 이 경우에는 DOMContentLoaded 이벤트가 필요하지 않을 수 있습니다.
<head>
<script async src="analytics.js"></script>
</head>
<body>
<p id="example">Loading...</p>
</body>
analytics.js는 페이지 로딩과 상관없이, 가능한 한 빨리 실행되어야 하는 분석 코드입니다. DOM 조작이 필요하지 않다면, DOMContentLoaded 이벤트는 필요하지 않습니다.
4. 자바스크립트 파일에서 반복되는 내용은 추상화를 해주는 것이 좋습니다.
API_KEY는 코드에서 노출되면 안 되지만, 숨기려면 서버가 필요합니다. (수업 예정 !)
기존 코드
const API_KEY = "2adc56b466647529ed2d583a496656e9";
function showLoadingSpinner() {
document.getElementById("geneve").style.display = "none";
document.getElementById("seoul").style.display = "none";
document.getElementById("loadingSpinner").style.display = "block";
document.getElementById("loadingSpinner2").style.display = "block";
}
function hideLoadingSpinner() {
document.getElementById("geneve").style.display = "block";
document.getElementById("seoul").style.display = "block";
document.getElementById("loadingSpinner").style.display = "none";
document.getElementById("loadingSpinner2").style.display = "none";
}
function onGeoOk(position) {
const url = `https://api.openweathermap.org/data/2.5/weather?q=Genève&appid=${API_KEY}&units=metric`;
const url2 = `https://api.openweathermap.org/data/2.5/weather?q=Seoul&appid=${API_KEY}&units=metric`;
showLoadingSpinner();
fetch(url)
.then((response) => response.json())
.then((data) => {
const weather = document.querySelector("#weather #geneve h3:first-child");
const city = document.querySelector("#weather #geneve h3:last-child");
weather.innerText = `${data.name} 🇨🇭
${data.weather[0].main} / ${data.main.temp} ℃ `;
})
.finally(() => {
hideLoadingSpinner();
});
fetch(url2)
.then((response) => response.json())
.then((data) => {
const weather2 = document.querySelector("#weather #seoul h3:first-child");
const city2 = document.querySelector("#weather #seoul h3:last-child");
weather2.innerText = `${data.name} 🇰🇷
${data.weather[0].main} / ${data.main.temp} ℃ `;
})
.finally(() => {
hideLoadingSpinner();
});
}
function onGeoError() {
alert("Can't find you. No weather for you.");
}
document.addEventListener("DOMContentLoaded", function () {
showLoadingSpinner();
navigator.geolocation.getCurrentPosition(onGeoOk, onGeoError);
});
피드백 코드
const API_KEY = "2adc56b466647529ed2d583a496656e9";
navigator.geolocation.getCurrentPosition(onGeoOk, onGeoError);
function toggleLoadingSpinner(city, isShow) {
document.querySelector(`#${city.id}`).style.display = isShow ? "none" : "block";
document.querySelector(`.loading.${city.id}`).style.display = isShow ? "block" : "none";
}
async function fetchWeather(city) {
const res = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city.q}&appid=${API_KEY}&units=metric`);
const data = await res.json();
const weather = document.querySelector(`#${city.id} h3:first-child`);
weather.innerText = `${data.name} ${city.flag}
${data.weather[0].main} / ${data.main.temp} ℃ `;
}
function onGeoOk() {
const cities = [
{ id: "geneve", flag: "🇨🇭", q: "Genève" },
{ id: "seoul", flag: "🇰🇷" , q: "Seoul" }
];
cities.forEach(async city => {
toggleLoadingSpinner(city, true);
await fetchWeather(city);
toggleLoadingSpinner(city, false);
})
}
function onGeoError() {
alert("Can't find you. No weather for you.");
}
코드 뜯어보기


isShow 매개변수:
• 불리언 값(true 또는 false)으로, 로딩 스피너를 보여줄지(true) 또는 숨길지(false)를 결정합니다.
로딩 스피너 숨기기 또는 표시하기:
• document.querySelector(.loading.${city.id}).style.display = isShow ? "block" : "none";
• 여기서 .loading.${city.id}는 해당 도시와 관련된 로딩 스피너의 클래스를 나타냅니다. loading 클래스는 스피너에 적용되며, 도시의 id를 통해 어떤 도시의 로딩 스피너인지 식별합니다.

navigator.geolocation.getCurrentPosition(onGeoOk, onGeoError):
사용자의 현재 위치를 가져오기 위해 Geolocation API를 사용합니다. 성공적으로 위치 정보를 가져오면 onGeoOk 함수가 호출되고, 실패하면 onGeoError 함수가 호출됩니다.
• cities.forEach(async city => {...}):
이 부분은 cities 배열에 있는 각 도시(제네바와 서울)에 대해 아래의 작업을 비동기적으로 처리하는 루프입니다.
• forEach는 배열의 각 요소에 대해 함수를 실행하는 메서드입니다. 여기서는 각 도시 객체(city)에 대해 비동기적으로 날씨 정보를 가져오고 로딩 스피너를 관리하는 작업을 수행합니다.
• async 키워드는 이 함수가 비동기 함수임을 나타내며, await 키워드를 사용할 수 있게 해줍니다.
• toggleLoadingSpinner(city, true);:
• 이 함수는 해당 도시의 날씨 정보를 가져오는 동안 로딩 스피너를 화면에 표시하고, 날씨 정보 요소를 숨깁니다.
• true는 로딩 스피너를 표시하고, 날씨 정보를 숨기라는 의미입니다.
• await fetchWeather(city);:
• 이 줄은 fetchWeather(city) 함수를 호출하여 해당 도시의 날씨 정보를 API에서 가져옵니다.
• await 키워드는 이 비동기 작업이 완료될 때까지 다음 코드를 실행하지 않고 기다립니다. 즉, 날씨 정보가 완전히 로드될 때까지 기다린 후 다음 작업이 실행됩니다.
• toggleLoadingSpinner(city, false);:
• 날씨 정보가 성공적으로 로드된 후, 로딩 스피너를 숨기고 날씨 정보 요소를 화면에 다시 표시합니다.
• false는 로딩 스피너를 숨기고, 날씨 정보를 표시하라는 의미입니다.


5. 자바스크립트에서 css 속성을 제어하는 것 보다 따로 제어해주는 것이 좋을 수 있습니다.
기존 코드
자바스크립트 파일에서 스타일 display를 제어하고 있습니다
document.getElementById("geneve").style.display = "none";
document.getElementById("seoul").style.display = "none";
피드백 코드
이 방식은 직접적으로 style.display = "none"을 설정하는 것과는 달리, 클래스를 통해 스타일을 제어하기 때문에 스타일 관리가 더 유연해집니다. 예를 들어, 나중에 "hidden" 클래스에 다른 스타일을 추가하거나 변경할 수 있으며, 이러한 변경이 해당 클래스를 사용하는 모든 요소에 적용됩니다.
document.getElementsById("geneve").classList.add("hidden");
document.getElementsById("seoul").classList.add("hidden");
#geneve .hidden {
display: none
}
#seoul .hidden {
display: none
}