it공부 (이야기)

MDN 문서 여행: fetch()메서드를 이해해보자. /// XMLHttpRequest, Asynchronous, promise, then 그외 무수한 개념들...

cantor 2023. 3. 10. 21:12

목차링크


1.시작: fetch가 뭐야?

2. XMLHttpRequest: 선배님들이 사용한 api통신 오브젝트

3.Promise & then : fetch의 밑바탕 +@ 영단어 resolve 

4. Synchronous & Asynchronous:동기와 비동기, Promise의 개발이유

5. 결론: 영상속 코드 이해하기

 

 

들어가며..

 

이 글은 특정 개발지식을 익히기에 적합한 글이 아닙니다.

 

문서에 쓰여있는 개념들을 읽으면서 실시간으로  내용을 써넣었기 때문에,

개념의 정돈보다는 흐름에 집중했습니다.

 

항상MDN 문서를 위키 탐독하듯이 돌아다니면서 공부를 합니다.

인상적인 개념 몇가지만 포스팅으로 남기는것이 늘 아쉬워

공부 과정 전체를 글로 남겼습니다.

 

시간 넉넉하고 심심하신분만 읽어보시길 바랍니다.



사실 쓰다가 절반이상을 날려먹어서 
추려써서 저도 공부하는 흐름을 완전히 살리지는못했습니다.

 

 

 

 

 

미리읽는 후기

처음엔 단순히 fetch() 메서드를 이해하려고 했다. 하지만 길고 긴 웹서핑끝에

XMLHttpRequest, Promise, Asynchoronous등 다양한 프로그래밍 개념을 익히고

예제를 다루어보면서 코드의 구조와 더불어 기술변화의 간략한 역사까지 살펴보게 되었다.

 

 

 

 

 

 

1.시작: fetch가 뭐야?

 

fetch() 메서드를 사용하는 강의

https://www.youtube.com/watch?v=xF8I1oe5A0M 

 

 

영상소개

위 영상은 자바스크립트로 만든 웹페이지에

날씨 정보를 제공 기능을 추가하는법을 다룬다.

 

채널의 주인장은 스페인어가 모어이면서  영어를 사용해서 

한국어 구사자에게 프로그래밍 언어를 가르치는..굉장히 흥미로운 사람이라 요즘 즐겨듣고있다.

 

다루는 코드의 기능:

1. geolocation api의 getCurrentPosition 메서드를 통해

현재 웹페이지에 접속한 유저의 위치정보 (위도, 경도)를 입력 받는다.

 

2. 다시  openweather api (이하 OWM)에게 위치 정보가 포함된 요청을 발송해

위치에 대한 날씨정보를 받아온다.

 

발단

 2번의 과정에서 fetch() 메서드를 사용한다.

하지만 깊은 설명은 해주지 않는다.

 

문서예제를 한번 훑어주고 "자바스크립트의 강력함"을 알려주고 싶어서 

이 기능을 보여준다는데..아무리 클론코딩 강의라곤 하지만 정말 뜬금없다.

 

*이사람을 비방할 의도는 없다.  강의도 정말 즐겨듣고있다.

 

 

 

궁금한 부분의 코드

const onGeoOk = function (position) {
    // lat과 lon이 위치정보를 나타낸다. 
    const lat = position.coords.latitude;
    const lon = position.coords.longitude;
    
    // OWM url에 위치정보와 API_KEY를 집어넣는다
    const url =  "https://api.openweathermap.org/data/2.5/"
    + `weather?lat=${lat}&lon=${lon}&appid=${API_KEY}&units=metric`;
    

    /** 아래코드의 역할:
    * 1. OWM 서버에 요청을 보낸후,
    * 2. 받은 응답을 json 형식으로 변환 
    * 3. 이후 json 내부의 데이터에서 도시와 날씨정보를 추출한다.
    */
    
    fetch(url)
    .then((response) => response.json())
    .then((data) => {
      city.innerText = data.name;
      weather.innerText = `${data.weather[0].main} / ${data.main.temp}`;
    });
}

 

 

그중에서도 바로 이 부분

fetch(url)
    .then((response) => response.json())
    .then((data) => {
      city.innerText = data.name;
      weather.innerText = `${data.weather[0].main} / ${data.main.temp}`;
    });

 

 

fetch의 반환값이 뭐기에 then() 이라는 메서드를 두번이나 사용하는걸까?

 

 

심지어 response는 then의 콜백함수인자로 사용되는중이다..

 

나는 이 코드의 바탕이굉장히 궁금했다.

 

이전까지의 강의에 이런 통신메서드는 한번도 본적이 없었고 , 연쇄적으로

적용되어있는 then 메서드의 모양새가 너무 신기하게 생겼기 때문이다.

 

 

궁금한점.

1. fetch () 메서드와 then() 메서드의 역할은 무엇인가?

2. then()에 입력된 콜백함수의 인자: (response,  data)는 또 어디서 오는것일까?

 

 

 

일단 fetch의 사전적 의미는 가져오기이다.

 

 const url =  `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${API_KEY}&units=metric`;
    fetch(url)

그렇다면 이 부분을 살펴볼 때 아무래도 fetch(url)은 url에게 요청을 보내고,

이후 url로부터 받은 응답을 처리하는 메서드인 것 같다.

 

fetch의 mdn 공식문서를 살펴보자.

https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

MDN설명:

Fetch Api는 프로토콜의  요청 및 응답(requests and responses)과 같은 부분에

대한 접근 및 조작 기능을 제공하는 자바스크립트 인터페이스입니다.

 

fetch() 메서드를 사용한다면 네트워크를 가로질러

비동기적으로 resources(자원, 정보)를 가져오는 쉽고 논리적인 방법을 제공합니다.

 

과거 이러한 기능은 XMLHttpRequest를 통해 수행되곤 했습니다.

하지만 Fetch가 더 나은 대안입니다... (중략)

 

 

 

*fetch를 통해 요청을 전송 할 수 있고, 도착한 응답에도 접근할 수 있는 듯하다.

 

 

XMLHttpRequest (이하 XHR)에비해 간편해졌다고한다.

 

그럼 XHR의 코드는 좀더 복잡할테니, 과정이 한줄 한줄 서술되어있을거같다.

더 디테일한 이해를 얻기위해 해당 객체를 사용하는 코드를 살펴보자

 

 

 

2. XMLHttpRequest: 선배님들이 사용한 api 통신 오브젝트

https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest

 

MDN 설명:
XMLHttpRequest (이하 XHR)는 서버와 상호작용하기 위한 오브젝트입니다.
전체 웹페이지를 새로고침하지 않고도 일부분을 업데이트할 수 있게 해 줍니다.

AJAX 프로그래밍에 사용되며,
만약 당신이 양방향 통신 커뮤니케이션을 원한다면, WebSockets (웹소켓)이 더 나은 대안일 수 있습니다.

 

 

 

*AJAX와 webSocket는 지금 필요한게 아닌거같다. 나중에 알아보자.

 

일단 해당문서에서 페이지 왼쪽 날개의 Using XMLHttpRequest 를 예제 코드를 읽고,

직접 사용해보았다.

 

처음에 나왔던 fetch() 메서드와 같은 프로그램에서 동일한 기능을 수행하는 코드이다.

const req = new XMLHttpRequest();
req.onreadystatechange = () =>{
    if (req.readyState === XMLHttpRequest.DONE) {
        console.log(req);
        const data = JSON.parse(req.responseText);
        console.log(data);
        city.innerText = data.name;
        weather.innerText = `${data.weather[0].main} / ${data.main.temp}`
    }
}
req.open("get", url);
req.send();

위의 코드를 한줄한줄 설명하자면 이렇게 된다.

 

const req = new XMLHttpRequest();

request(요청)의 준말인 req라는 이름을 갖는 새로운 XHR 오브젝트를 생성했다.

 

req.onreadystatechange = () =>{
    if (req.readyState === XMLHttpRequest.DONE) {
        console.log(req);
        const data = JSON.parse(req.responseText);
        console.log(data);
        city.innerText = data.name;
        weather.innerText = `${data.weather[0].main} / ${data.main.temp}`
    }
}

req에 onreadystatechange 이벤트 핸들러를 추가했다.

 

onreadystatechange란, req의 readyState라는 속성(property) 값이 번경되는 이벤트를 감지하는 

XHR 전용 이벤트 핸들러이다.

 

readyState는 다음과 같은 5개의 상태를 갖는다.


0: "UNSENT" - XHR 오브젝트가 생성되었으나, open 메서드가 실행되기 전이다.
1: "OPENED" - open 메서드가 실행되었다. 하지만 send메서드는 실행되지 않았다.
2: "HEADER_RECEIVED"  - send 메서드가 실행되어 요청이 전송되었다.
3: "LOADING" -  현재 응답이 도착하고 있다.
4: "DONE" - 전체 응답이 도착했고, 사용할 수 있다.

 

*onreadystatechange는 readyState의 값이 번경될 때마다 실행된다.

 if (req.readyState === XMLHttpRequest.DONE)

위의 if 문의 조건은 readyState가 4: DONE 일 때, 즉 응답이 순조롭게 도착했을 때 ture값이 된다.

그렇게 되는 경우 블록안의 코드가 실행된다.

 

console.log(req);
const data = JSON.parse(req.responseText);
        console.log(data);
        city.innerText = data.name;
        weather.innerText = `${data.weather[0].main} / ${data.main.temp}`

console.log(req) 와 console.log(data)를 통해 

req (XHR 오브젝트)와  data가 어떻게 생겼는지  살펴보려 한다.

 

console.log(req);

 

XHR오브젝트의 성분을 확인할 수 있다.

위의 사진을 잘 살펴보면 중간즈음에

readyState속성값이 4인것을 확인할 수있다.

 

그 아래에 response 및 responseText 속성을 보면

OWM로부터 응답으로 도착한 데이터가 오류없이 전송되기위해 string으로 변형된 json인것을 볼 수 있다.

 

이를통해 XHR오브젝트는 서버로부터 받은 응답을

자신의 response 속성에 직접 저장한다는 사실도 알 수 있었다.

 

const data = JSON.parse(req.responseText);
        console.log(data);

이제 JSON.parse 메서드를 통해 다시 객체로 변환된 응답을 살펴보자

 

 

console.log(data);

응답을 오브젝트로 복원했다.

 

OWM으로부터 전송받은 데이터.

 

 

이제 아래 코드의 실행을 좀더 확실히 이해할 수 있었다.

 

  city.innerText = data.name;
weather.innerText = `${data.weather[0].main} / ${data.main.temp}`

weather은 오브젝트 하나를 성분으로 갖는 배열이다.

data.weather[0].main은 현재 날씨를 가져온다.

 

습도, 압력, 온도, 최고 최저기온등이 표시되고있다.

data.main.temp로 온도를 가져온다.

 

- 여기까지 XHR 오브젝트에는 무엇이 들어있고,

응답을 어떻게 listen 하는지 알 수 있었다. - readystatechange

 

그리고 OWM의 API 문서를 따로 읽지 않았음에도 불구하고

서버로부터 받는 응답의 형식 및 정보들도 개략적으로 이해 되었다.

 

*feels_like 는 아무래도 불쾌지수인거같다.

 

 

 

이제 그다음  open 메서드와 send 메서드를 살펴보자.

req.open("get", url);
req.send();

open 메서드는 XHR에 담긴 요청값을 초기화(설정)해주는 메서드이다.

 "get" 과  url을 인자로 받고 있다

 

 send는 요청을 보내주는 메서드이다.

 

 

 

 

너무 날림 설명같은데

XHR공식문서의 서술 그대로다.

 

 

https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest

MDN 문서설명:
XMLHttpRequest.open()
요청을 초기화합니다.

XMLHttpRequest.send()
요청을 보냅니다. 요청이 만약 비동기라면( 기본설정 )
이 메서드는 요청을 전송하자마자 반환합니다.

.

 

 

 

*비동기?? 모르는게 하나 더나왔다.

 

 

 

그래도 일단은 전체코드를 다시 한번 요약해 보자.

const req = new XMLHttpRequest();
req.onreadystatechange = () =>{
    if (req.readyState === XMLHttpRequest.DONE) {
        const data = JSON.parse(req.responseText);
        console.log(data);
        city.innerText = data.name;
        weather.innerText = `${data.weather[0].main} / ${data.main.temp}`
    }
}
req.open("get", url);
req.send();

1. XHR 오브젝트 생성

 

2.  onreadystatechange 이벤트 핸들러
- XHR 오브젝트의 readyState 속성값(상태)의 변화에 반응한다.

 readystate 속성이 4. DONE으로 완료될 경우 서버로부터 받은 응답에 대해 필요한 작업을 수행한다.

 

3. open메서드: 요청을 초기화한다.(설정)

 

4. send 메서드: 요청을 발송한다.

 

 

 

 

XMLHttpRequest의 작동을 이해 했다.

 

이제 이것을 바탕으로 fetch를 살펴 보자.

 

fetch(url) // url에 (OWM 서버) 요청을 전송한다. 
           // 반환받은 요청 (response) 은 then 메서드 콜백인자로 들어온다
    .then((response) => response.json())
    
    /**
    *response의 json() 메서드를 호출한다.
    *응답내용을 json으로 변환 한 후
    *다음 then의 콜백인자로 넘겨준다.
    */
    .then((data) => { // 넘겨받은 값을 통해 필요한 정보를 추출한다.
      city.innerText = data.name;
      weather.innerText = `${data.weather[0].main} / ${data.main.temp}`;
    });

 

 

response의 json() 메서드를 호출하는 부분이 굉장히 이상하다.

json() 메서드..?  방금 XHR의 response에는 그런거 없었는데...

 

 

 

*다시 이해가 안가기 시작한다.

 

XHR을 이용할때는

req.response에 저장되었던 오브젝트는 그냥 OWM 에서 전송한 날씨정보를 담고있었다.

 

.json 메서드 같은거 없어요..

 

위의 오브젝트는xhr.response가 아니라 xhr.responseText속성에 저장된 값이지만

 

 

*참고 코드

const data = JSON.parse(req.responseText);

req.response도 같은 내용을 담고있다.

 

 

console.lof(req.response)

 

 

json() 메서드는 어디에서 온걸까?

3. promise & then : fetch의 밑바탕 +@ 영단어 resolve 

 

 

MDN 문서에서 json() 메서드를 찾아보았다..

https://developer.mozilla.org/en-US/docs/Web/API/Response/json

MDN문서설명:
json() 메서드는 Response 인터페이스의 메서드로, Response  stream을 끝까지 읽어냅니다.
그 후 body text를 JSON으로 resolve 하는 프로미스를 반환합니다.

이 메서드의 이름이 json() 임에도 불구하고, 결과는 JSON이 아닌 JSON을 parsing한
자바스크립트 오브젝트임을 기억하세요.

 

 

 

* Response 인터페이스, stream,  resolve, 프로미스 라는 새로운 키워드를 얻었다.

 

관련 MDN 문서를 차례대로 살펴봐야겠다.

 

 

 

1. Response 인터페이스

https://developer.mozilla.org/en-US/docs/Web/API/Response

MDN문서 설명:
Response 인터페이스는 Fetch API의 요청에대한 응답을 표현합니다.

 

 

* 그러니까 XHR의 response 속성에 저장된 값이랑, then() 메서드의 콜백인자로 쓰이는 response는 

전혀 다른 데이터라는 이야기가 된다. 

 

 

json() 메서드는 body text를 JSON으로 파싱한다니까 body 속성도 가져와보았다.

Readable Stream의 body 구성물?

 

2. stream

https://developer.mozilla.org/en-US/docs/Web/API/Streams_API

 

이 문맥에서 사용된 stream은

데이터자원을 잘게 부수어 연속적으로 전송하는것을 의미한다.

 

 

*stream 은 video+ audio 와같은 결합 미디어, 전송흐름 등등 다양한 뜻을 가지고있다.

 

결론:

 

fetch api에서 등장하는 response 인자는 데이터 응답을 표현하는 인터페이스이다.

그 내부에 포함되어있는 json() 메서드는

전송데이터 흐름에서 읽을수있는 body컨텐츠를 JSON 형식으로 파싱한다.

 

뭔가 잘 이해가 안가지만... 첫술에 배부를 수 없는법이다. 

이쯤에서 정리하고 다음 키워드로 넘어가보겠다.

 

3. resolve와 promise

body text를 JSON으로 resolve 하는 프로미스를 반환합니다.

 

resolve?? 나는 개발문서에서 이런 동사를 처음봐

 

 

resolve는 그냥 보편적으로 사용되는 동사가 아니었다.

프로미스의 동작을 나타내는 용어이다.

 

 

 

 Promise라는 오브젝트에 resolve() 라는  메서드도 존재한다.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve

MDN문서설명:

resolve() 는 Promise 오브젝트의 static 메서드입니다. 프로미스에 주어진 값을 "resolves" 합니다.

만약 값이 프로미스이면, 프로미스를 반환합니다, 또 값이 thenable( then을 적용할수 있는)이기까지 하면,
미리 준비된 두개의 콜백과 함께 then() 메서드를 호출합니다.; 그렇지않다면, 프로미스는 값과 함께 이행됩니다.

 

이 설명은 하나도 이해가 안간다..

예제를 살펴봐야겠다.

 

*일단 resolve가  static 제어자로 선언된 메서드라는것은 알겠다.

static 제어자는 한번 다룬적있다. 

 

Static Modifier(스태틱 제어자)란? 개념 및 Typescript 예제

static? 타입스크립트의 static 제어자: 클래스의 구성요소에 선언에 사용되는 제어자이다. static으로 선언된 property(특성)이나 behavior(메서드)는 클래스에 직접 속하게 되며, 인스턴스가 아닌 클래스

batcave.tistory.com

static 메서드는 오브젝트이름을 통해 호출할 수있는 메서드를 말한다.

굳이 new키워드로 인스턴스를 만들지 않아도 사용 할 수 있다.

 

 

 

문서에있는 예제를 토대로 내가 이해한 내용은 이렇다.

 

프로미스가 어떤 값을 resolve 한다

=프로미스로 메서드를 호출할때, 그 메서드의 콜백함수 인자로 값을 넣어준다.

 

 

 

.......말이 너무 어렵다! 코드를 한번 보자.

 

// 9를 promise9에 resolve 시키는 코드
const promise9 = Promise.resolve(9);

// 9는 then의 인자인 콜백함수의 인자가 되었다.

promise9.then((value) => {
  console.log(value);
});

 

콘솔 출력 결과.

 

내친김에 처음에 fetch() 메서드처럼 2중 then도 만들어 보았다.

//궁금한 코드

fetch(url)
    .then((response) => response.json()) // return이 없어도 다음 then의 콜백은
    .then((data) => {                    //인자를 넘겨받는다.
      city.innerText = data.name;
      weather.innerText = `${data.weather[0].main} / ${data.main.temp}`;
    });

const promise9 = Promise.resolve(9);


//2중 then을 사용한 코드.

promise9.then(first => {
        first + 1
          }).then(second => {
                console.log(second)
                });
/*  예상 출력 결과
*
* fist + 1 이 second의 인자가 된다.
* 콘솔에는 10이 출력된다.
*/

 

 

 

 

하지만 위의 코드는 예상대로 작동하지 않았다.

 

 

 

undefined가 출력되었다.

 

first + 1 (9) 가 두번째 then의 콜백인자인 second로 전달되지 않았다.

 

 

 

이문제는 첫번째 then의 콜백에 return을 추가하여 해결할 수 있었다.

return을 추가했다.

 

왜 return이 필요하지?

 

 

 

분명 fetch()메서드를 사용할때 return 같은 것은 존재하지않았다.

fetch(url)
    .then((response) => response.json())
    .then((data) => {
      city.innerText = data.name;
      weather.innerText = `${data.weather[0].main} / ${data.main.temp}`;
    });

 

 

위에서 json() 메서드 문서를 다시 읽고왔다.

 

json()메서드는 body text를 JSON 형식의 데이터로 resolve 하는 프로미스를 반환한다.

 

아하!

 

정리해보면 이렇다.

 

1. then()의 콜백함수의 반환값은 다음에 오는 then의 콜백인자가 된다.

 

2. json() 메서드는 body text를 JSON 데이터로 변환 한 후

다음에오는 then 메서드의 콜백인자로 넣어준다.

 

3. 원래 then의 콜백함수끼리 인자를 넘겨주려면  return 이 필요한데,

json() 메서드를 사용한경우엔 필요없다.

 

 

영단어 하나 보고가자.

resolve

 

이거 뉘앙스가 되게 미묘한 단어다.

개발문서에서는 처음보기에 설명하고 넘어갈 필요가 있는것 같다.

 

보통  resolve를 해결하다, 결심하다, 다루다, 안정시키다 의 뜻으로

설명하는 책이나 웹페이지가 많다. 하지만 많은 원서에서 등장하는 resolve를 

빠르게 이해할 수없다. 4개의 뜻이 판이하게 다르기 때문이다.

 

resolve 의 중심뜻은 "다루다, 이야기 하다" 이다.

굳이 한국어로 바꿔보자면 "이야기 하다" 가 가장 가까운 뜻인거같다.

 

주로 원서에서 등장하는 resolve는 다음의 느낌으로 쓰인다.

 

"이야기 끝났어"- 결심했다, 해결했다.

"미리 이야기 되었어."- 안정된 문제다.

"이야기 할거야" -다룰거다. 

 

*프로미스가 값을 resolve 한다는것은 값을 다루다: 데리고 다닌다 정도로 받아들였다.

 

그럼 이제 then에 대해 살펴볼 시간인 것 같다.

 

 

then 메서드의 공식문서.

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/then

MDN설명:
프로미스 오브젝트의 then() 메서드는 두 개의 콜백함수를 인자로 받습니다. 하나는
프로미스가 fulfilled(이행충족)되었을 때  다른하나는 rejected(거절) 되었을때 사용됩니다.

then은 즉시동치의 프로미스 오브젝트를 반환합니다.
이는 프로미스 메서드의 chain call(연쇄 호출)을 가능하게 합니다

.

 

 

*then 메서드는 프로미스 오브젝트의 메서드이고,

그 반환값 또한 프로미스임을 알 수 있다.

 

 

 

다시 fetch() 메서드가 사용된 코드를 살펴보자. 

fetch(url)
    .then((response) => response.json())
    .then((data) => {
      city.innerText = data.name;
      weather.innerText = `${data.weather[0].main} / ${data.main.temp}`;
    });

 

 반환값에 then() 메서드를 사용하는것을 보면

fetch() 메서드의 반환값도 프로미스 임을 유추할 수 있다.

 

fetch(url)이 프로미스를 반환하면

그 위에 then 메서드를 적용하고 다시 그위에 then 메서드를 적용한다.

이게 바로 연쇄 호출 과정일 것이다.

 

이제 프로미스 오브젝트를 알아보자.

 

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

공식문서 * 프로미스는 asynchronous(비동기) 작업의 완료 혹은 실패를 표현하기 위한 오브젝트입니다.

 

 

*비동기에 대해 알아볼 필요가 있다.

 

4. Synchronous & Asynchronous:동기와 비동기, Promise의 개발이유


비동기소개 공식문서로 이동하자

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing


MDN문서 설명:
이 문서에서는 비동기 프로그래밍 의 방법 및 이유를 설명하고, 
비동기 적용 및 실행에 관한 자바스크립트의 간략한 역사를 서술합니다.

선수지식: 자바스크립트 함수 및 이벤트 핸들러

목적: 자바스크립트의 비동기 개념에 대해 익숙해지고 ,
동기 (synchoronous) 와의 차이점 및 비동기가 필요한 이유를 이해합니다.

비동기 프로그래밍:프로그램이 현재 진행 중인 작업이 끝날 때까지 기다리지 않고,수행도중에 다른 이벤트들에도 반응할 수 있게 허용하는 기술입니다.한번 그 작업이 종료되면, 프로그램이 결과를 표시할 것 입니다.

 

함수와 이벤트 핸들러 정도는 알고 있다.

 


드디어 선수 지식이 필요 없는 최초 기반지식문서까지 내려 왔다.!! 

이문서를 잘 읽고 소화해보자.

 

 

 

Introducing asynchronous Javascript 문서를 요약

 

Synchronous Programming: 동기 프로그래밍


브라우저는 자바스크립트의 코드를 1번부터 차례대로 한줄한줄 실행한다.
이때 일반적으로 써진 코드의 동작이 완료되기 전에는 뒤의 코드가 실행되는 것을 허용하지 않는다.

간단한 예를 들어보자.

console.log("first");
console.log("second");
console.log("third");

위의 코드는 위에서부터 아래대로 순서대로 진행된다.


console.log("first")의 실행이 완료되어야
console.log("second")가 실행된다.

세 번째 줄도 마찬가지로 두 번째 줄이 완료되어야 실행된다.

 

이렇게 순차적으로 먼저 쓰인 코드의 실행이 끝나야 다음줄이 실행되는 프로그램을 

 동기(synchronous) 프로그램이라 한다.

 

*함수의 경우에는 동기 함수라고 한다.

콘솔 출력결과

코드가 쓰인 순서와 동일한 순서로 값이 출력된 것을 볼 수 있다.

 

ASynchronous Programming: 비동기 프로그래밍

 



프로그램 내부에 먼저 선언된 코드의 실행이 완료되기 전에

다음 줄의 실행을 허용하는 코드가 존재한다면, 비동기(Asynchronous) 프로그램이라 한다.

 

*함수는 비동기 함수라 한다.

 

다음 예제를 살펴보자.

/** setTimeout: (콜백함수, 시간)
*두번째 인자에 쓰인 숫자만큼의 시간이 지나면
*첫번째 인자에 쓰인 함수를 실행하는 함수
*
* 숫자의 단위는 Millisecond, 즉 (천분의 1초이다.)
*/

setTimeout( ()=> {
console.log("first")
console.log("this is asynchronous")
}, 1000);

console.log("second");
console.log("third");

만약 위의 코드가 순서대로 진행된다면

1초 대기후 다음과 같은 순서로 콘솔에 텍스트가 출력될 것이다.

 

 first -> this is asynchronous -> second -> third  

 

 

하지만 setTimeout은 비동기 코드이다.

 

다시말하자면 본인의 작업이 완료되기 전에

다음줄의 코드가 먼저 실행되는 것을 허용한다.

콘솔 출력결과

위의 사진을 보면 second-> third -> first -> this is asynchronous

순서로 문자열이 출력되었다.

 

 

setTimeout 의 콜백이 마지막에 실행된 것이다.

 

 

이런 비동기 코드는 왜 등장하게 되었을까?

 

 

동기프로그램의 한계

 

동기식 프로그램의 한계1. blocking 현상: 응답속도 지연의 발생

 

클라이언트의 자원 및 성능과 관계없이 

완료시간이 긴 코드가 있다.

 

그런 코드가 포함된 프로그램이 동기식으로 실행된다면

시간및 컴퓨터자원의 낭비가 막심하다.

 

내컴퓨터의 성능과 무관한 실행시간을갖는 코드가 있다고?

있다.

 

지금껏 공부한 request(요청) 과정이 바로 그 예이다.

 

서버에게 요청을 보낸 후 응답을 받고 그에 대한 처리까지 수행하는 코드가 있다고 해보자.

이 코드의 완료는 서버로부터 응답을 받고 나서 진행된다.

 

서버에 문제가있어서 응답이 도착하지 않는다면, 다음 코드들은 영원히 실행되지 않는다.

 

응답이 빠르게 도착하더라도 그 동안 프로그램의 다른 부분은 멈춰있다면 

이는 분명히 자원의 낭비이다.

 

 

 


추가 예시로 구글검색서비스를 생각해 보자.

*다음과 같은 가정을 해보자

 

A. 구글에 무언가를 검색하면, 결과가 출력될 때까지 다음과 같은 단계들을 거친다.

 

 

1. 유저의 요청 받아들이기

2. 데이터 수집하기

3. 수집된 데이터를 통해 전송할 페이지를 구성하기

4. 유저에게 응답하기

 

각 과정의 작업처리 시간은 모두 동일하며, 각 과정은 한번에 하나만 수행된다.

(2번 과정 두개가 동시에 시행될 수 없다.)

 

 

B. 1, 2, 3, 4 각 과정에 사용되는 컴퓨터자원 및 코드가 달라서

각 과정을 독립적으로 동시에 실행할 수 있다

 

 

C. 구글은 단 1대의 서버만 1개의 동기식 프로그램으로 운영한다.

 

-한 명의 유저가 검색을 한 경우, 그 유저에게 응답을 완료하고 나서야 다음유저의 검색요청을 받아들일 수 있다.

 

 

 

위의 세가지 가정을 전제로 돌아가는 검색 서비스의 실행 순서는 다음과 같다.

검색요청 1 -> 2 -> 3 -> 4 -> 검색요청 1 -> 2 -> 3 -> 4

 

문제점

 

 

가정 B. 1, 2, 3, 4 각 과정에 사용되는 컴퓨터자원 및 코드가 달라서
각 과정을 독립적으로 동시에 실행할 수 있다

에 의해 

 

한 유저에게 응답을 전송하면서,

다른 유저의 검색 요청을 받아들이고, 데이터를 수집할 수 있다.

 

하지만 가정 C에 의해,

C.구글은 단 1대의 서버만 1개의 동기식 프로그램으로 운영한다.

응답까지의 과정이 전부 완료되어야 다음 요청을 받아들일 수 있다.

 

 

비동기식으로 운영된다면?

 

이제 1, 2, 3, 4 가 각각 비동기적으로 실행된다고 생각해 보자.

4개의 검색요청에 대한 작업을 동시에 수행할 수 있게 된다.

 

검색요청 1  ->  2  ->  3  ->  4
               #      #      #
               #      #      #
       검색요청 1  ->  2  ->  3  ->  4
                      #      #      #
                      #      #      #  
             검색요청  1  ->  2  ->  3  ->  4
                             #      #      #
                             #      #      #  
                    검색요청  1  ->  2  ->  3  ->  4              

4개의 검색요청이 진행되는것을 표현한 그림이다.

#에 세로로 선을 그었을때, 이어진 부분들은 동시에 실행된다.
 
ex) 첫번째 요청의 4, 두번째요청의3,
세번째요청의2, 그리고 마지막요청의1 과정은 동시에 실행된다.

이상적으론 4개의 요청을 동시에 처리할 수 있다.

 

동기식 프로그램의 낭비를 비동기식 프로그램은 겪지않는다. 

 

 

*위의 구글검색에 대한 예시는 순전한 창작이다.

 mdn 문서에선  매우 느린 응답속도를 가진 엄청 긴 코드예제를 통해 설명하고 있다.

 

 

 동기식 프로그램의 한계2. 이벤트 감지 불가

 

 

하나의 코드가 완료되어야 다음 코드가 실행된다.

 

단 한개의 실행문으로 짜인 프로그램이 아니라면

프로그램이 실행 내내 동작중인 함수가 존재할 수 없다.

 

즉 특정 이벤트가 발생하면 처리해 주는 함수를 만들 수 없다.

 

왜냐하면 해당 함수도 결국 종료 되어야 다음 코드가 실행되므로

다른 코드가 실행중일땐 이벤트의 감지가 불가능하다.

 

 

*맞다. 바로 프로그램 실행 내내 이벤트를 감지하는 addEventListener 등의 이벤트리스너가 대표적인 

비동기 동작 코드이다.

 

이렇게 동기프로그램의 한계를 알아보았다.

 

그렇다면 비동기 함수가 필요한건 알겠는데,

어떤 형식의 코드가 비동기 함수일까?

 

자바스크립트에서 어떤 코드를 만나면 비동기 작업을 한다고 추측할 수 있을까?

 

비동기코드 (함수)의 구현 형태:  종료를 알리는 콜백함수.

 

동기 코드는 그 코드의 다음줄이 실행되는 것으로 완료 여부를 알 수 있다.

 

하지만 비동기 코드의 완료여부는 다음 코드의 실행과는 상관이 없다.

 

그래서 비동기 함수는 코드가 완료되었음을 알리는

(혹은 완료될 경우 실행해야 하는) 콜백함수를 인자로 집어넣는다.

 

 

 

예시 1. addEventListener
btn = document.querySelector("button")

const callBack = function () {
console.log("click is detected")
}


btn.addEventListener("click", callBack);

 위의 코드에서 addEventListener는 "click" 이벤트를 감지하는 역할을 한다.

callBack함수를 실행함으로써 해당 동작(click 감지)이 완료되었음을 알리는 역할을 한다.

 

 

 

예시 2. setTimeout
setTimeout( ()=> {
console.log("first")
console.log("this is asynchronous")
}, 1000);

setTimeout은 특정 시간의 지연을 측정하는 함수이다.

두 번째 인자에 입력해 준 시간이 경과될 경우, 동작의 수행을 알리는 콜백함수 (첫 번째 인자)를 실행한다.

 

 

예시 3. http.createServer(). listen()
import http from "http";
import express from "express";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));
app.get("/", (_, res)=> res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));


const httpServer = http.createServer(app);

const handleListen = () => console.log(`Listening on http://localhost:3000`);

httpServer.listen(3000, handleListen);

위의 코드는 express 모듈을 사용하여 브라우저 랜더링 과 라우팅에 대한 설정을 하고

http서버를 생성 및  실행하는 코드이다.

 

마지막에 있는 listen() 메서드가 비동기 함수로 서버를 직접 실행시키는 역할을 맡고 있다.

 

 

const handleListen = () => console.log(`Listening on http://localhost:3000`);

httpServer.listen(3000, handleListen);

서버가 실행되고 포트 3000을 통해 데이터를 주고받을 준비가 되면,

콜백함수 handleListen을 통해 서버가 준비되었다는 의미의 메시지를 콘솔에 출력해 준다.

 

 

지금까지 설명한 것을 요약해 보자.

 

동기프로그램: 

 

코드가 쓰여진 순서에따라 위에서부터 아래로 차례대로 실행되는 프로그램

 

현재 진행중인 실행문이 완료되어야만 다음 문장이 실행된다.

 

문제점:

1. blocking 현상. 오랜 시간이 걸리는 코드가있다면, 전체 프로그램의 진행이 지연됨

 

2. 이벤트 감지불가: 한번에 하나의 문장만 실행될 수 있기때문에, 

프로그램 동작 내내  특정이벤트를 감지하고있는 함수를 사용할 수 없음.

 

비동기프로그램: 

 

자신의 작업이 완료되지 않았어도 다음 문장을 실행시키는것을 허용하는 비동기코드가 들어있는 프로그램

 

자바스크립트에서는 콜백함수를 이용해 비동기함수의 종료를 알린다.

 

 

 

문서를 읽다보면 callback의 한계와 프로미스 오브젝트의 등장배경을 알 수 있다.

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing

 

mdn문서 설명:
이벤트 핸들러는 단지 적절한 시기에 작동되는것으 예상되는 콜백함수일 뿐입니다.
그리고 콜백함수는 그냥 다른함수의 인자로 들어가는 함수를 일컫는 말입니다.

방금 위에서 본것처럼, 콜백함수들은 자바스크립트에서 비동기함수를 구현하는 주된 방법이었습니다.

하지만 콜백함수들 그 자신도 또다른 콜백함수를 인자로 받아들이는 형태라면 굉장히
이해하기 힘든 코드가 만들어집니다.
이는 비동기 작업을 여러개의 연속된 비동기함수로 분해하여 구현할때 아주 자주 발생하는 일입니다.

 

 

 직접한번 만들어 봤다.

const async1 = function (callback, time) {
    setTimeout(callback, time); 
};

const async2 = function (callback, time) {
    setTimeout(callback, time); 
}

const async3 = function (callback, time) {
    setTimeout(callback, time); 
};

const callbackPyramid = function () {
    async1(()=> {
        async2(()=>{
            async3(()=>{
                console.log("pyramid is amazing!");
            }, 3000)
        }, 2000)
    }, 1000)
}

callbackPyramid();

위의 코드에서 async1, 2, 그리고 3은 이름만 바꾼 setTimeout 함수이다.

 

callbackPyramid 함수내부에서 setTimeout은 그 자신을 콜백으로 사용하고있다.

정상적으로 작동한다.

 

예제로 만든 간단한 코드임에도 불구하고, 구조를 파악하기가 쉽지 않다.

 

각 함수별로 마지막에 나오는 숫자가 두번째 인자고,

첫번째인자가되는 콜백함수는 그사이에 쓰여있다.

 

알아보기 쉽게 색깔을 씌어보았다.
async1(()=> {
        async2(()=>{
            async3(()=>{
                console.log("pyramid is amazing!");
            }, 3000)
        }, 2000)
    }, 1000)

 

 

실제 프로그램에서 이러한 함수에서 오래가 발생할경우, 

어느 단계에서 발생한 오류인지 파악하는일은 굉장히 골치아플것이다.

 

바로 이러한 이유로 프로미스 오브젝트가 등자하게되었다.

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing

MDN문서:
우리는 콜백속의 콜백을 콜 해야하기에,  엄청난 중첩의 함수를 만들었습니다.
이를 읽고 디버깅하는것은 굉장히 어려운 일입니다.
그래서 이 코드는  "콜백 지옥" 혹은 "저승의 피라미드"로 불립니다.

이렇게 콜백을 중첩하면 오류를 다루기도 굉장히 힘들어집니다:
 오류는 보통 이 "피라미드"의 꼭대기 하나에서 해결되지 않습니다. 
당신은 각 층별로 오류처리를 해야합니다.  

이러한 이유로 대부분의 현대 비동기 API들은 콜백패턴을 사용하지 않습니다.
대신 현대 자바스크립트 비동기의 근본은 Promise(프로미스) 오브젝트에 있습니다.

그리고 그것은 다음글의 주제입니다!.

 

정말 읽어보고 싶게 생겼다. 빨리 다음글로 이동하자.

 

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Promises

MDN문서: 
프로미스는 현대 자바스크립트 비동기 프로그래밍의 근본을 담당합니다.
이는 비동기 함수에 의해 반환되는 오브젝트이며, 현재 작업의 상태를 대변합니다.

프로미스가 호출자에게 반환되었을때, 보통 작업은 아직 끝나지 않은 경우가 많습니다.
하지만 프로미스 오브젝트는 결과적으로 작업완료나 실패에 도달하게 해주는 메서드들을 가지고있습니다.

지난 게시글에서, 우리는 비동기함수의 콜백 패턴을 다뤘습니다. 
콜백함수를 인자로 가지고있는 비동기함수를 호출하면, 함수가 완료되는 즉시 콜백함수가 호출되어
작업의 종료를 알립니다.

프로미스기반의 API에서는, 비동기함수는 프로미스 오브젝트를 반환합니다.
당신은 여러가지 핸들러 함수를 프로미스 오브젝트에 붙여놓을 수 있습니다.
이것들은 작업이 성공하거나 실패했을때 실행됩니다.

 

* 프로미스기반 API의 비동기 함수처리과정:

1. 비동기함수는 프로미스를 반환한다.
2. 작업의 성공 혹은 실패여부에따라 프로미스의 상태가 바뀐다.
3. 바뀐 상태에 따라 특정 핸들러들이 실행된다.

 

사용된 기술은 전혀다르지만 

XHR의 readystatechange 이벤트 핸들러와 비슷해보인다.

 

고로 프로미스는 비동기 함수를 처리하기 위해 도입된 특수 오브젝트라는것을 알 수 있었다.

 

 

프로미스의 상태를 좀더 살펴보자.

 

 

프로미스의 상태 및 상태별 작동 핸들러 프로미스의 상태

MDN문서 :
1. pending:( 매달림, 지연)

프로미스는 생성되었지만, 아직 비동기 함수가 실패하거나 성공하지 않은 상태이다.
fetch() 메서드를 호출한경우에 pending 상태의 프로미스가 반환되며,
서버로 보내는 요청이 만들어지고 있는 중이다.

2. fulfiled: (이행됨)

비동기 함수의 실행이 성공적으로 완료된 상태이다.
만약 프로미스가 fulfilled 상태로 번경시, then()메서드가 호출(작동)된다.

3. rejected: (거절됨)

비동기 함수가 실패한 상태이다.
만약 프로미스가 rejected 상태로 번경시, catch()메서드가 호출된다.


+@settled (고정됨) fulfiled 및 rejected를 함께 묶어 settled라고 표기한다.

*성공이나 실패가 의미하는 상태는 전적으로 API의 종류에달려있다. fetch()는 서버로부터 404 같은 오류메시지가 담긴 응답을 받더라도, 본인이 요청을 성공했다고 판단해 성공 상태의 프로미스를 반환한다. 

 

 

프로미스의 동작을 보여주는 그림.

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise




위의 mdn문서에선 fetch api를 사용하여 프로미스에대한 예제를 보여주고 있다.

끝이보인다!!

이제 fetch 메서드에대한 설명을 읽어보자.

https://developer.mozilla.org/en-US/docs/Web/API/fetch

MDN문서:
fetch() 메서드는 네트워크로부터자료를 가져오는 프로세스를시작합니다. 만약 response(응답)이 사용가능하게 된다면, fulfilled 상태의 프로미스를 반환합니다.

프로미스는 당신의 요청에대한 응답을 나타내는 "응답 오브젝트"를 resolve 합니다.

fetch()의 프로미스는 네트워크오류가 발생했을때만 reject를 작동시킵니다. 즉 404와같은 http 오류에는 reject를 이행하지 않으므로, then() 핸들러를 이용해 Response.ok 혹은 Response.status 속성을 확인해야 합니다.

 *http서버는 성공적으로 응답을 보내면 HTTP 200 OK (성공 응답 코드를) 보낸다.
Response.ok 나 Response.status 를 확인하라는것은 http의 응답상태 코드를 확인하라는 것이다.


쭉쭉 읽어 나가자.

fetch 메서드의 인자는 Request() 생성자와 같다. 즉 요청을 인자로 받아들이는 것이다.

이제 마지막으로 then 메서드를 살펴보자.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then

then() 은 프로미스 오브젝트의 메서드로 두개의 인자를입력받는다.

1번 인자는 프로미스가 fulfilled 상태가 되었을때 호출될 콜백함수이고
2번 인자는 reject 상태가 되었을때 호출되는 콜백이다.

then의 콜백함수에는 각각 프로미스가 데리고다니는 값이 인자로 사용된다.

 

끝! 이제 처음의 코드를 완전히 이해할 준비가 끝났다!

5. 결론: 영상속 코드 이해하기

 

fetch(url) //1
    .then((response) => response.json())
    .then((data) => {
      city.innerText = data.name;
      weather.innerText = `${data.weather[0].main} / ${data.main.temp}`;
    });

1. fetch(url)은 OWM 서버로 요청을 보낸후 프로미스 오브젝트를 반환한다.

 

서버로부터 응답이 도착하면 프로미스의 상태는 "fulfilled"로 변하며, response 인터페이스를 resolve 한다

(다음에 호출되는 메서드의 콜백인자로 response를 대입한다.)

 

2.프로미스의 상태가 "fullfiled" 가 되었으니 남은 then메서드 두개가 차례대로 실행된다.

(chaining promise)

첫번째 then은 콜백인자로 넘겨받은 response인터페이스의 json() 메서드를 실행시킨다.
이 메서드는 응답  stream의 body 텍스트를 JSON 형식으로 파싱한다.

이후 그 값을 then메서드가 반환하는 프로미스가 resolve 하게 한다.

두번째 then은 넘겨받은 JSON 데이터에서 도시, 날씨, 온도정보를 추출해서
html 태그의 innerText 속성에 저장한다.


끝!



읽어주셔서 감사합니다.
오류나 오탈자를 발견하신분은 댓글을 남겨주세요!