썸네일 출처와 참고자료
https://velog.io/@chosule/javascript-promise
오늘은 개인 프로젝트에서 주로 모듈화를 위한 리펙토링을 작업했다. 작업했던 JS 파일은 한 파일 안에 페이지 로딩, api calling, ui 업데이트 등을 모두 한꺼번에 실행하는데 이게 사실 나중에 고칠때 찾기도 힘들고 코드도 정신이 없고 그렇다.
그래서 비슷한 기능의 함수/로직들을 묶어 최종적으로 한 파일 안에 두고 그 묶어진 파일만 html에서 불러오는 과정을 모듈화의 예시라고 할 수 있다.
내 파일은 이벤트리스너들을 아래와 같은 script에 다 모아두고, ui.js 와 api.js로 나누어서 정리를 했다.

정리하다보니 script에는 ui.js만 import를 하고, ui.js에는 api.js만 import를 하게끔 정리가 되었다 (의도하진 않았다). 다른 파일들을 import하는 방법은 아래와 같은 방법으로 했다.

필요한 기능만 export로 두어서 다른 파일에서 import하는 방식.
예전에 사용한 파이썬처럼 했을때는 import * as ui from "./ui.js" 이렇게 했는데, 튜터님이 저 코드를 보시고는 파이썬을 써본적 있냐고 물으시더라ㅋㅋ. 저게 파이썬에 적합한 import라는 건 처음 알았다 (저것만 써봤으니....이렇게 경험이 부족한 감자이다). 그래서 위의 코드로 바꾸었다.
추가적으로 ui.js도 저렇게 생겼다.

함수 앞에 다 export를 써놓았고, 나중에 고칠게 있으면 특정 함수만 찾아서 고치면 된다.
알고 있던 eventListener 사용법은 아래 코드의 첫번째 줄과 같은데
showBMBtn.addEventListener("click", () => {showBM()});
showBMBtn.addEventListener("click", showBM);
// showBMBtn.addEventListener("click", showBM());
두번째 줄도 같은 코드였다. 신기했던 것이 3번째 줄은 에러를 띄우는데 그 이유는 eventListener가 함수 실행 값이 아닌 함수를 참조하는 매개변수를 받아야하기 때문이다. 새삼 ()있고 없고가 크구나 싶었던 예시였다.
event.target은 이벤트 위임을 하면서 익혔는데 currentTarget이라는 친구도 있다는 것을 알게 되었다.
둘의 차이는 event.target은 정확히 무엇을 눌렀는지 보여주고
event.currentTarget은 이벤트리스너 (핸들러 모두)가 부착된 요소를 가르킨다. 코드로 보자면
<body>
<header>
<h1>이거슨 제목</h1>
</header>
</body>
document.addEventListener("click", (event) => {
console.log("event.target =>", event.target);
console.log("event.currentTarget =>", event.currentTarget);
})
document에다가 eventListener를 달고 페이지 제목을 눌렀을때

위와 같이 뜨는 것을 볼 수 있다. 만약에 리스너를 header에 달았다면 currentTarget이 header로 뜬다
header.addEventListener("click", (event) => {
console.log("event.target =>", event.target);
console.log("event.currentTarget =>", event.currentTarget);
})

이벤트 위임을 하다가 모든 리스너들을 상위 요소에 배정했는데 이런 경우가 생겼다. 영화 카드를 클릭하면 모달이 뜨게 하는 부분에서 event.target.matches을 사용했다. event.target.matches()는 class라면 .을 붙여서, id라면 #를 붙여서 클릭된 요소가 정확히 같은 class 혹은 id를 가지고 있는지 확인해준다. 그래서 사용했는데...
// 모듈화 하기 전 코드
movieListSection.addEventListener("click", () => {
console.log(event.target);
if (event.target.matches(".movie-card")) {
console.log("영화 카드가 클릭됨");
openModal();
}
});
근데 아무리 카드를 눌러도 콘솔에 영화 카드가 클릭됨 문구가 뜨지 않는것이다. 대신 event.target에는 카드 안의 요소만 클릭되고 있었다. 영화 카드가 아래와 같이 생겼을 때
<div class="movie-card">
<img src="" alt="">
<h3>영화 제목</h3>
<p>베스트 평점</p>
</div>
event.target에 뜨는 것은 img, h3, p였다. 빈틈 없이 꽉꽉 채워놔서인지 movie-card div가 찍히는 일은 없었다. 그럼 누른 것의 부모 요소를 찾는 기능은 없을까 하다가 찾아낸 것이 event.target.closest()였다. 공식 문서를 살펴보면
https://developer.mozilla.org/ko/docs/Web/API/Element/closest
주어진 CSS 선택자와 일치하는 요소를 찾을 때까지, 자기 자신을 포함해 위쪽(부모 방향, 문서 루트까지)으로 문서 트리를 순회합니다.
가장 가까운 부모의 요소와 일치할때까지 순회하므로 img, h3, p의 부모인 movie-card만 적어서 확인하면 영화 카드를 눌렀을때 openModal이 작동하도록 구현할 수 있다.
movieListSection.addEventListener("click", () => {
if (event.target.closest(".movie-card")) {
console.log("영화 카드가 클릭됨");
openModal();
}
});
콘솔에도 문구가 제대로 찍히고 영화 상세보기 창도 잘 뜨는 것을 볼 수 있다.

JS 강의에서 했지만 제대로 이해하지는 못한...ㅎㅎ...
동기 방식은 요청을 보낸 후 결과를 기다린 후 다음 작업으로 넘어가는 방식이다. 말로만 봐도 비효율적인 느낌이 드는 방식이지만 순서 하나는 기가 막히게 지킨다는 장점이 있다. 아래의 콘솔에 나오는 방식이다.
console.log("손님1 커피 주문");
console.log("손님1 커피 제조 시작");
console.log("손님1 커피 제조 완료");
console.log("손님2 커피 주문");
console.log("손님2 커피 제조 시작");
console.log("손님2 커피 제조 완료");
비동기 방식은 요청을 보낸 후 결과를 기다리지 않고도 바로 다음 작업으로 넘어가는 방식이다. 따라서 요청만 보내놓고 다음 작업을 하다가 이전 작업이 완료되는 대로 결과가 나온다. 결과를 기다리는 시간을 줄여주는 효율적인 방법이지만 응답의 도착 시간을 예상할 수가 없기 때문에 순서가 꼬일 가능성이 크다.
console.log("손님1 커피 주문");
console.log("손님1 커피 제조 시작");
setTimeout(() => {
// 손님1 커피 제조 진행중...
console.log("손님1 커피 제조 완료");
}, 2000); // 2초 후에 실행
// (0초여도 비동기라 아래 줄이 먼저 실행됨)
console.log("손님2 커피 주문");
console.log("손님2 커피 제조 시작");
// ...
비동기 코드이기 때문에 다음 로직이 바로 실행된다 (기다려야 하는 시간이 0이어도 마찬가지)
비동기 함수는 동기 함수/로직에 순서를 양보한다고 생각하면 편하다.
블로킹 작업은 현재 실행 중인 코드가 완료될 된 후에 다음 작업으로 넘어간다.
즉, 시간이 소요되고 동기적으로 작동하는 부분이 있다면 이는 blocking에 해당된다.
function blockingTask() {
const start = Date.now();
// 3초 동안 블로킹
while (Date.now() - start < 3000) {
// 시간이 지나기만을 기다립니다.
}
console.log("블로킹 작업 완료");
}
console.log("작업 시작"); // 1
blockingTask(); // 2
console.log("작업 완료"); // 3
Non-blocking은 blocking과 다르게 시간이 소요되지만 비동기적으로 작동하는 부분이라고 할 수 있다.
function nonBlockingTask() {
setTimeout(() => {
console.log("논블로킹 작업 완료");
}, 3000);
}
console.log("작업 시작"); // 1
nonBlockingTask();
console.log("작업 완료");
사용자 정의 함수는 call Stack에 잘 들어가지만 (동기적인 코드들)
Web APIs는 다른 저장소에 있다가 시간이 지나면 Task Queue에 적재된다. Call Stack이 비었을 때 Wep Api의 queue의 대기하고 있는 task들을 실행한다.
동기적인 일들이 먼저 처리된 후 비동기적인 일들이 대기하는 순서대로 실행된다. 비동기는 늘 동기 뒤에!
그림을 그려주실때는 잘 이해했는데 말로 하려니 엉망진창이다...
Promise: 내부적으로 작업에 대한 약속을 하는 것 (성공/실패) 두 결과의 분기처리도 담을 수 있다
resolve->성공, reject->실패를 의미하며 각 상황에 맞는 메세지 혹은 로직을 부여하면 그것이 비동기적으로 실행된다. 둘이 나오기 전의 상태 (혹은 실행되지 전의 상태)는 대기라고 하며 완료된 상태는 fulfilled (이행됨)이다
function orderCoffee(order) {
return new Promise((resolve, reject) => {
console.log(`${order} 커피 주문을 받았습니다.`);
// 커피 제조 시간 (2초)
setTimeout(() => {
if (order) {
resolve(`${order} 커피가 준비되었습니다.`);
} else {
reject("주문 오류: 커피 주문이 잘못되었습니다.");
}
}, 2000);
});
}
// Promise 사용
orderCoffee("카푸치노")
.then(result => {
console.log(result); // 커피 제조 완료 메시지 출력
})
.catch(error => {
console.error(error); // 에러 메시지 출력
});
then은 약속한 작읍을 이행하고나서, 이행된 결과를 then으로 받는 것 만약에 잘못되면, error 코드를 띄운다
Fetch도 일종의 Promise이다
setTimeOut은 Promise가 아님
따라서 모든 비동기 함수를 Promise라고 할 수는 없다.
then(함수) 에서 () 안의 함수가 비동기로 처리되는 것, Promise를 생성하는 것과 그 안의 함수는 동기적으로 함수가 된다.
new Promise(동기함수)
.then(비동기함수)
.catch(비동기함수);
예시코드를 보며 순서를 예측해보자
console.log('1');
const promise = new Promise((resolve, reject) => {
console.log('2');
resolve('3');
}).then(result => console.log(result))
.catch(err => console.log(err);
;
console.log('4');
내 예상은 1423 였으나
정답은 1243이다.
Promise의 작업 시작 자체는 동기적으로 실행이 되기 때문에 resolve의 부분이 아닌 console.log('2')부분은 생성과 동시에 실행이되기 때문에 1 다음은 2. 그 다음은 비동기 함수이므로 다음 동기적 부분인 4로 넘어간 후 비동기 로직인 resolve부분 즉 console.log('3')이 실행된다.
^ 말을 거지 같이 해놓았는데 아래처럼 챗GPT가 잘 설명해준다 (영어임)
https://chatgpt.com/share/67160d46-5204-8013-9969-ef15beea57c6
함수 앞에 async를 붙여줌으로써 비동기 함수를 만들 수 있으며 이의 return 값은 Promise.resolve()이다.
async function myAsyncFunction() {
return "Hello, Async!"; // 즉, 이 부분이 Promise.resolve 부분 (fulfilled)
}
myAsyncFunction().then(result => {
console.log(result); // "Hello, Async!"
});
그 다음 키워드는 await (꼭 async function 내에 써주어야 함), 프로미스가 해결될 때까지 비동기 함수의 실행을 일시 중지해준다.
아래의 코드와 같이 쓰며, 개인 프로젝트에서 유용하게 써먹은 방법이다.
async function fetchData() {
const res = await fetch("~~");
return await res.json();
}
하지만 fetch에서 발생하는 에러처리가 필요하므로 아래처럼 바꿔주면
export async function fetchData() {
try {
const res = await fetch("~~");
// 에러 메세지 생성
if (!res.ok) {
// res 메타데이터 말고 body part 저장
throw new Error(
`HTTP error! Status: ${res.status}`
);
}
return await res.json();
} catch (error) {
console.warn(error);
}
}
과제에 사용할 코드를 완성시켰다
async/await의 장점
1. 가독성 향상
2. 디버깅 용이
3. 직관적 에러 처리
가끔 여러 비동기 작업을 동시에 처리하고 그 결과를 기다릴 때 Promise.all을 사용하기도 한다. 개인 프로젝트에서 사용했는데 아래와 같다
// 북마크 로드 함수
export async function showBM() {
// 북마크 정보 불러오기
let bookmarkIds = JSON.parse(window.localStorage.getItem("bookmarkIds"));
// 북마크된 영화 데이터 불러오기
let bmResults = await Promise.all(
bookmarkIds.map(async (id) => {
return await fetchMovieById(id);
})
);
// 가져온 영화 리스트에 붙이기
printMovies(bmResults);
}
이전에 url 관련해서 쓴 TIL이 있었는데 거기에 연장선 같은 내용
url의 구성은
https://example.com/path/to/resource?query=string
JS에서 네트워크 요청하는 함수는 두개인데 XMLHttpRequest 객체와 fetch 함수이다
근데 저 X 뭐시기는 동작 방식도 복잡하고 이전에 사용하던 방법이라고 해서 아래에 예시 코드만 붙여보고 이해는 나중에....할 수 있겠지?
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.error("Error:", xhr.status);
}
}
};
xhr.send();
근데 await를 안 사용하는 fetch도 사실... 읽기 어렵다
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.err("Error:", error));
이상으로 졸렸지만 어떻게든 작성 노트를 마쳐보겠다.

Promise랑 async/await를 사용하는 것은 사실 감은 잡았는데 위와 같이 Promise then만 사용하는 문법은 도저히 이해가.... 아직은 조금 어렵다ㅠㅠ
왜 벌써 월요일인 것일까...!