프론트 공부

웹 개발 기본 (fetch 함수 다루기) 공부 기록

홍구리당당 2024. 1. 18. 21:21

웹 개발의 꽃 promise... 제대로 공부해보자.

단순히 감으로 axios 쓰면서 땜빵했는데, 제대로 공부할 필요성을 느꼈다.

코드잇 인강을 듣고 fetch 함수와 promise 객체에 대해 공부한 것을 기록한 글이다. 주요 키워드는 JSON, promise, fetch, async, await 이다!!

1. 목차

2. 배운 것

1. fetch 함수

fetch 함수는 서버로 request를 보내고 response를 받아오는 함수이다. fetch는 기본적으로 js 내장 메소드지만, 외부 라이브러리인 axios에 비해 기능이 좀 빈약하다. 그래서 보통 실무에선 axios 라이브러리를 많이 사용하는 모양이다. 어쨌거나 fetch 자체는 웹 통신의 기본 함수라는 것.

fetch("https://www.google.com")
    .then((res) => res.text())
    .then((result)=> {console.log(result);});

기본적인 fetch 함수 사용법. 인자로 url을 넘겨주고, 프로미스 체이닝으로 then 메소드를 연결해준다.

여기서 then 메소드는 비동기 실행 함수로, 이전 promise 함수로부터 promise 객체를 리턴받아야만 실행된다!!! 그래서 fetch로부터 res를 받은 후에야 1번 then 메소드가 실행되고, 그다음 then이 다시 result 를 리턴하면 2번째 then이 실행되는 형식.

그리고 then 메소드 안에 인자로 쓰이는 arrow function을 콜백함수라 한다. 콜백 함수의 정확한 의미는, 어떤 조건이 만족되었을 때 실행될 비동기적 함수 이다. then 메소드는 이 콜백 함수를 등록하는 역할을 한다.(사실 콜백은 그냥 함수의 파라미터로 전달되는 함수를 의미하는 용어고, 그 안에서 비동기 실행되는 콜백과 동기 실행되는 콜백이 있는 것임. 여기서는 비동기 실행 콜백만을 일컫음.)

코드 작동 순서

  1. fetch 함수가 실행되면서 url로부터 request를 받고, response를 받음
  2. 그리고 then 메소드는 순서대로 자기가 인자로 받은 콜백 함수를 등록함.
  3. fetch함수가 response를 promise 객체 형태로 return
  4. fetch가 리턴한 promise 객체를 1번 then 메소드가 콜백함수를 실행하여 promise 객체를 리턴.
  5. 2번 then 메소드는 1번 then 메소드가 리턴한 promise 객체를 받아 콜백함수를 실행시킴.
2. 웹과 url

Web이란?

World Wide Web의 줄임말로, 전 세계 연결망을 뜻한다.

url이란?

uniform resource locator의 줄임말. 직역하면 웹에 존재하는 수많은 데이터들 중에서 특정 데이터를 나타내는 문자열이라는 뜻!! url은 프로토콜을 나타내는 스키마, 메인 도메인 주소를 나타내는 호스트, 하위 페이지 경로를 나타내는 패스, 옵션으로 넣을 수도 있고 안 넣어도 되는 쿼리문으로 이루어졌다. 이 부분은 네트워크를 공부하면서 더 자세히 알아보자.

프로토콜이란?

통신을 하는 두 주체가 지켜야 할 통신 규약. 이 부분은 네트워크를 공부하면서 더 자세히 알아보자... 일단, 웹 통신 프로토콜에는 http와 https가 대표적으로 있고, 요즘엔 보안 문제때문에 https를 더 많이 쓰는 추세 정도로 알아두자.

3. json

서버가 우리에게 response를 보낼 때 데이터 값으로 html을 보낼 수도 있고, json 데이터를 보낼 수도 있음.

json은 javascript object notation 의 줄임말로, js 언어 문법과 유사하게 만들어진 데이터 포맷이다. (그냥 확장자 같은 것임. png 확장자가 있고 jpg 확장자가 있고 jpeg 확장자가 있듯이 데이터에서 .xml이든 .json이든 확장자가 다양하다.)

실제로 json 파일을 보면 js에서 객체를 만드는 형식과 비슷하게 생겼지만, js 언어로 만들어진 건 아니라서 다른 부분이 좀 있다.

어쨌든 우리가 서버와 통신할 때 'use 정보를 주세요', 라든가 '로그인한 유저의 찜 목록 리스트 정보를 주세요,' 하고 무언가 데이터를 요청할텐데 그럴 때 서버는 json이나 xml 등 일정 형식의 데이터를 준다. 그럼 우리가 이 데이터 값을 읽어서 쓰면 되는 것이다.

요즘엔 json을 많이 쓰다보니 일단 json만 공부해보자면...

  1. json 파일은 언어가 아니라 그냥 데이터 형식이기 때문에, 주석을 넣을 수 없다.
  2. json 파일에 넣는 데이터 객체의 property는 반드시 쌍따옴표로 감싸줘야 한다.
  3. 값이 문자열일 경우 반드시 큰 따옴표로 감싸줘야 한다.
  4. json 파일에선 undefined, NaN, infinity 등 특정 데이터 형을 쓸 수 없다. 근데 null은 가능함.
const member = {
  "name": 'Michael Kim',
  "height": 180,
  "weight": 70,
  "hobbies": ['Basketball', 'Listening to music']
};

json의 자세한 내용은 여기를 참고하면 좋다!! https://www.json.org/json-en.html

그래서 우리가 서버로부터 받은 json 형태의 데이터를 본격적으로 사용하려면, js 객체 형태로 바꿔줘야 한다!! 왜냐하면 우리의 멍청한 컴퓨터는 json 데이터를 객체가 아니라 하나의 문자열로 취급하기 때문이다.

json -> js 객체로 바꾸려면, JSON 객체의 parse 메소드를 사용하면 된다.

fetch("https://jsonplaceholder.typicode.com/users")
    .then((res) => res.text()) // 먼저 text로 바꿔주고
    .then((result)=> {const users = JSON.parse(result)}) // js 객체로 바꿔주기
    .then((result)=> {console.log(users)});

axios 라이브러리에선 response를 받으면 자동으로 js 객체로 바꿔주기 때문에 이 과정을 거칠 필요는 없다.

4. 메소드

서버가 DB 작업을 할 때 크게 4가지 작업이 있다.

  • C: create
  • R: read
  • U: update
  • D: delete

이 CRUD 작업을 하기 위해 클라이언트 측에선 request에 메소드를 붙여 보낸다.

예를 들어 request에 GET 메소드를 붙인다면 서버는 READ 처리를 하고,

request에 DELETE 메소드를 붙여 보낸다면 서버는 DELETE 처리를 하는 식이다.

이 CRUD 작업에 대응되는 메소드는

  • create -> POST
  • read -> GET
  • update -> PUT
  • delete -> DELETE

이다. (이외에도 PATCH HEAD 같은 다른 메소드도 있긴 함.)

어쨌거나 우리가 서버에게 'user가 회원 탈퇴를 했으니 user 정보를 없애는 작업을 해주세요~' 라고 부탁하려면 회원 탈퇴 버튼에 DELETE user request를 보내는 기능을 넣어두는 것이고, 유저가 버튼을 눌러 서버에게 DELETE 요청이 갔다면 서버는 요청을 읽어 user Database에서 delete 작업을 하는 것이다!!

메소드는 request의 head 부분에 붙인다

request와 response는 head와 body 부분으로 나뉜다.

request를 보낼 때 메소드는 head 부분에 담아서 보내면 된다.

delete, get 메소드를 사용할 때엔 body가 따로 필요하진 않지만, post, put 메소드를 사용할 때엔 무슨 값을 저장하란 건지, 무슨 값을 업데이트하란 건지 알려줘야 하므로 body에다 데이터를 넣어서 request를 보내게 된다.

이 head 안에는 여러 header들이 들어있다. (header은 head 안에 담겨있는 프로퍼티 - 값 쌍 하나를 일컫음)

이 중 method property를 가지고 있는 header을 보면, 그의 값으로 get, post, put, delete 등 우리가 적은 메소드들이 담겨있을 것.

get, post, put, delete request 보내보기

사실 get url은 우리가 위에서 작성한 fetch("url") ... 그대로이다. get 메소드는 기본 그 자체라서, 딱히 method 명시를 안하고 그냥 url만 적어도 작동한다.

문제는 post, put, delete 메소드이다. 얘네를 다룰 때엔 본격적으로 request의 헤드 안에 method 값을 명시하고 필요에 따라 body 값도 채워넣어야 한다.

  1. post 메소드
    • 상황: 새 user가 회원가입을 해서, user의 정보를 서버로 post하는 상황.
      // user 정보가 담긴 js 객체
      const newMember = {
      name: "홍길동",
      email: "test@naver.com",
      age: 27,
      }
      

fetch("url", {
method: "POST", // post 메소드 명시하기!!
body: JSON.stringfy(newMember), // body에는 js 객체를 json타입으로 바꾼 값을 넣어준다.
}).then((result)=>{console.log(result)});


2. put 메소드
    - 상황: 유저가 setting 페이지에 들어가서 자신의 email 주소를 바꿨다고 해보자.
```js
// 새 유저 정보가 담긴 js 객체. put 메소드는 아예 덮어쓰기 하는 메소드라서 바뀌지 않는 나머지 정보들도 다 적어줘야 함.
const newMember = {
    name: "홍길동",
      email: "newTest@naver.com",
      age: 27,
}

fetch("url/333", { // user의 id 값을 뒤에 명시해서, 어떤 유저에 대한 데이터를 수정할지 알려줘야 함!!
      method: "PUT", // put 메소드 명시하기!!
      body: JSON.stringfy(newMember), // body에는 js 객체를 json타입으로 바꾼 값을 넣어준다. 
}).then((result)=>{console.log(result)});
  1. delete 메소드
    • 상황: 유저가 회원 탈퇴를 해서 회원 정보를 삭제해야 함!!
      // data 값은 필요 없다!!
      fetch("url/333", { // user의 id 값을 뒤에 명시해서, 어떤 유저에 대한 데이터를 삭제할지 알려줘야 함.
      method: "DELETE", // delete 메소드 명시하기!!
      }).then((result)=>{console.log(result)});
5. status code

request, response의 head에 들어갈 header로는 status code가 있다.

fetch 함수에서 status code를 확인하려면,

fetch('https://www.google.com/asdf')
  .then((response) => {
    console.log(response.status); // 없는 페이지라서 404 출력됨. // 참고로 숫자형이다.
      console.log(typeof response.status); // number 출력됨. string 형 아님!!!
  });

근데 axios에서 받는 response나 error 객체는 AxiosError 타입이라든가, 하여튼 타입이 좀 달라서 어떻게 status code를 따오는진 공식문서를 확인해봐야 한다.

6. content type

request, response 의 head에 들어갈 또다른 header 로는 content type이 있다.

지금 리퀘스트 / 리스폰스의 body 안에 들어가있는 데이터가 어떤 타입인지 알려주는 값이다.

body에 들어갈 데이터 값으로는 html, json 데이터 말고도 img나 동영상 등 여러 타입들이 있기 때문에 content type으로 미리 명시해주면 성능도 좋아지고 하여튼 굿~

content type에 들어가는 값은 주 타입(main type)/ 서브타입(sub type) 형식이다. 주 타입은 데이터 타입의 큼직한 형태, 서브 타입은 세밀한 확장자를 나타냄.

예를 들어 body 안에 들어있는 데이터가 일반 텍스트라면 text/plain
css 코드가 들어있다면 text/css
이미지 파일이 들어있는데 확장자가 gif라면 image/gif
png 확장자의 이미지 파일이 들어있다면 image/png
오디오가 들어있다면 audio/mp4 ...

text, image, audio, video가 아닌 나머지 타입들은 application이라는 주 타입에 속한다.

json 데이터도 application에 속하기 때문에, application/json 이라 쓰인다.

참고로 fetch는 아니고 axios 라이브러리에선 기본적으로 content type을 다 application/json으로 자동 설정해준다. 이미지 업로드나 특정 파일 형식이 요구될 때에나 content type을 설정해주면 되니 편하다. fetch에선 그냥 다 명시해야 함.

참고로 form 데이터를 보낼 때 프로필 이미지 값도 들어있다든가, 하여간 여러 타입들이 섞여있는 데이터를 보내야 한다면 multipart/form-data 값을 쓴다.

json 도 여러 데이터를 묶어서 객체 형태로 보낼 순 있지만, string, number, boolean 형태의 값만 넣을 수 있기 때문에 이미지, 오디오 등 다양한 확장자 파일을 묶어서 보내려면 multipart를 써야 함!!!

7. 비동기 실행

코드에는 동기 실행되는 코드와 비동기 실행되는 코드가 있는데, 동기 실행들이 먼저 실행되고 -> 그다음 비동기 실행들이 차근차근 진행된다!!!

console.log("start");

fetch("https://www.google.com")
    .then((res) => res.text())
    .then((res) => {console.log(res)});

console.log("end");

이 때 출력 순서는
1. start
2. end
3. res
순서이다.

코드 작동 순서는
1. console.log("start"); 에 의해 start 출력됨.
2. fetch 함수가 리퀘스트를 보내고 그 와중에 pending 상태의 promise 객체를 then 메소드에게 리턴해줌.
3. 1번 then 메소드가 객체를 받으면 그 안의 콜백 함수를 등록하고, promise 객체를 리턴함.
4. 2번 then 메소드가 1번 then으로부터 객체를 받아 콜백함수를 등록함.
5. console.log("end")에 의해 end 출력됨.
6. fetch함수가 response를 받은 상태라면, then 메소드로 등록해둔 콜백들이 순서대로 실행됨.

참고로 fetch 함수 뿐만 아니라, 비동기 함수들이 또 있음.

  1. setTimeout 함수는 특정 시간 후에 콜백 함수를 실행시키는 비동기 함수.
  2. setInterval 함수는 특정 시간 간격을 두고 콜백 함수를 실행시키는 비동기 함수.
  3. addEventListener 함수는 dom 객체에 콜백 함수를 등록해줌. event가 발생할 때마다 콜백이 실행되게 하는 비동기 함수이다!

setTimeout이나 addEventListener도 조건을 만족해야 콜백함수를 실행시키는 함수라서 비동기 함수임. 그렇지만 얘네는 promise 객체를 리턴하진 않는다.

fetch 함수는 promise 객체라는 것을 리턴하는 독특한 비동기 함수이기 때문에, fetch 메소드 이후로 then 메소드를 줄줄이 소세지처럼 나열해서 쓸 수 있는 것임!!

8. promise 객체

fetch 함수나 then 메소드가 자꾸 promise 객체를 리턴한다는데 이게 무슨 말일까??

promise 객체는 통신을 쉽게 하려고 만든 특별한 객체이다. 통신이 성공했는지, 에러인지, 통신 중인지 상태 정보와 통신을 통해 받은 데이터(에러가 나면 에러 데이터) 정보 값 등을 가진 객체!!

그니까 fetch나 axios 나 then 메소드들, 여튼 웹 통신을 하는 함수들은 promise 객체라는 걸 리턴하고, 이 promise 객체 안에는

promise = {
    웹통신 작업 상태: pending | fulfilled | rejected
      웹통신 결과물: 내가 원하는 데이터 값 | error 객체
}

이렇게 담겨있다는 것.

웹 통신 작업 상태는 pending, fulfilled, rejected 이렇게 3가지 상태가 있다.

맨 처음 fetch 함수가 .then 메소드에게 리턴하는 객체는 pending 상태의 promise 객체이다. 이걸 받은 then 메소드는 콜백 함수를 실행하지 않고, 일단 콜백 함수 등록만 해준다. 나중에 fetch 함수가 response를 받아오면 fetch 함수는 fulfilled 상태 (에러가 나면 rejected) 객체를 then에게 리턴해주고, then 메소드는 그 promise 객체의 데이터 값을 콜백 함수에게 넘겨주어 콜백을 실행하는 것.

then 메소드 내부 콜백함수가 어떠한 값을 리턴하면, 바깥에 있는 then 메소드는 그 값을 promise 객체로 감싸 다음 줄의 then 메소드로 넘겨준다.

만약 콜백함수가 아무 값도 리턴하지 않는다면, 데이터 값이 없고 fulfilled 상태이기만 한 promise 객체를 다음 줄의 then 메소드로 넘긴다.

이렇게 무조건 promise 객체를 받고 리턴하는 then 메소드의 특성으로 앞에서 썼듯 .then().then() ... 줄줄이 쓸 수 있는 건데 이게 바로 promise chaining이다.

참고로 text, json 메소드도 promise 객체를 리턴한다.

이런 promise chainaing은 순차적으로 처리해야 할 비동기함수들을 나열할 때 쓴다.

fetch(1).then(2).then(3) ... 이렇게 비동기 함수들을 순서대로 처리할 때!!

9. reject 처리 콜백

이때껏 위에선 response가 성공적일 때를 처리했지만, 만약 error를 리턴받거나 rejected 상태의 promise 객체를 받는다면 따로 처리해줘야 한다.

(axios 라이브러리를 쓸 때엔 함수 내부에서 try catch문으로 처리하고, react query를 쓰면 그냥 onError 이나 thronOnError 메소드를 쓰면 됨...)

fetch 함수를 쓸 때 rejected 객체를 처리하는 방법은 2가지가 있는데,

  1. then 메소드 두 번째 인자로 reject 객체 처리 콜백 함수 등록하기.

  2. try catch 문을 쓰기.

  3. then 메소드

    fetch("url")
     .then((res) => res.text(), (error) => {console.log(error)})
     .then((result)=>{console.log(result)})
  4. catch 문

    fetch("url")
     .then((res) => res.text())
     .catch((error) => {console.log(error)})

참고로 catch 메소드를 쓸 때엔 마지막에 써야 한다. catch 문은 error 객체가 담긴, fulfilled 상태의 promise 객체를 리턴하는 일종의 then 메소드이다. 그래서 만약 .catch().then() 순서로 쓴다면, catch 다음의 then 메소드는 error 객체를 담은 fulfilled 객체를 받아 처리해버림!! 그리고 무조건 실행해야 하는 코드에 대해서는 finally 키워드로 작성함.

10. promisify

promise 객체를 우리가 직접 만들어볼 수도 있다.

const customP = new Promise((resolve, reject) => {
    setTimeout(() => {reject(new Error("failed")); }, 2000};
});

p.catch((error) => {console.log(error)} );

이렇게 직접 promise 객체를 만드는 걸 promisify라 하는데, 실무에서 자주 쓰이진 않음. setTimeout 같은 promise를 쓰지 않는 비동기 실행함수를 promise 기반 코드로 변환해야 할때나 사용함...

참고로 promise 객체를 만들 때엔 상태를 정해둘 수도 있다.

const p = Promise.resolve("success"); // success 를 값으로 가진 fulfilled 상태의 promise 객체
const p = Promise.reject(new Error("failed")); // 에러 객체를 값으로 가진 rejected 상태의 promise 객체
11. 그 밖의 팁...

여러 url로 fetch를 해서 promise 객체를 여러 개 받아왔다고 치자. 이걸 동시에 다룰 경우엔?

all 메소드 : all은 여러 promise 객체를 기다렸다가 다 받으면 한 번에 취합하기 위해 쓴다. 하나라도 reject 상태가 된다면 전체 작업이 실패한 것으로 간주된다.

const p1 = fetch('https://url/1').then((res) => res.json());
const p2 = fetch('https://url/2').then((res) => res.json());
const p3 = fetch('https://url/3').then((res) => res.json());

Promise
  .all([p1, p2, p3])
  .then((results) => {
    console.log(results); // Array [p1, p2, p3] 출력됨!
  });

race 메소드 : promise 객체를 여러 개 받을 준비를 하는데, 그 중 제일 먼저 fullfilled 혹은 rejected 된 promise 객체가 있다면 그 promise 객체를 리턴함. 나머지는 무시!

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Success'), 1000);
});
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('fail')), 2000);
});
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('fail2')), 4000);
});

Promise
  .race([p1, p2, p3])
  .then((result) => {
    console.log(result); // success 출력
  })
  .catch((value) => {
    console.log(value);
  });

이외에도~ allSettled 메소드나 any 메소드 등 여러 가지가 있다.

12. async await

promise chaining은 .then 메소드를 줄줄이 나열하는 방식이라 배웠다.

그런데 이거 말고도 다른 방법으로 chaining을 할 수 있다. 바로 async await 키워드를 쓰는 것이다.

fetch("url")
    .then((res)=>res.text())
    .then((result)=>{console.log(result)});

async function print(){
      const res = await fetch("url");
      const result = await res.text();
      console.log(result);
}
print();

위 두 코드는 같은 의미.

axios를 보통 쓰기 때문에, .then 메소드보단 async 키워드를 쓸 일이 더 많을 것이다.

참고로~!!! await 문을 쓰면 그 뒤로 동기실행될 코드들도 올스탑된다.

async function fetchAndPrint() {
  console.log(2);
  const response = await fetch("https://jsonplaceholder.typicode.com/users");
  console.log(6);
  const result = await response.text();
  console.log(result);
}
console.log(1);
fetchAndPrint();
console.log(3);
console.log(4);
console.log(5); // 1 2 3 4 5 6 result 순서대로 출력됨.

const response = await~ 구문을 만나면 그대로 멈춰서 바깥 코드들이 실행됨.

만약 아래 코드처럼 작성하면 어떻게 될까??

async function fetchAndPrint() {
  console.log(2);
  const response = await fetch("https://jsonplaceholder.typicode.com/users"); 
  console.log(6);
  const result = await response.text(); 
  console.log(result);
}
console.log(1);
fetchAndPrint();
console.log(3);
console.log(4);
fetchAndPrint();
console.log(5); // 1 2 3 4 2 5 6 6 result result 순서대로 출력됨.

1 출력 -> 2 출력 -> await 만나서 밖으로 나오고 3 출력 -> 4 출력 -> 2 출력 await 만나서 밖으로 나오고 5 출력 -> 코드 끝났으니 다시 1번 fetchAndPrint 문으로 들어가서 await 처리되길 기다렸다가 6 출력 -> 또 await 만나서 밖으로 나오고 두 번째 fetchAndPrint 들어가서 await문 처리될때까지 기다렸다가 6 출력 -> await 만나서 다시 밖으로 나오고 첫번째 fetchAndPrint 만나서 await 처리되길 기다리고 console.log(result) -> 끝났으니 밖으로 나와서 두번째 fetchAnddPrint 문으로 들어가서 await 처리될때까지 기다리고 console.log함.

이 async await문으로 try, catch문을 작성한다면?

async function printAsync(){
    try{
        const res = await fetch("url");
          const result = await res.text();
          console.log(result);
    }catch(error){
        console.log(error);
    }finally{
          console.log("exit");
    }
}

printAsync();

참고로 async function도 promise 객체를 리턴한다!!! then 메소드나 마찬가지이다. 만약 async function 안에서 에러가 터진다면, rejected 상태의 에러 객체를 담은 promise 객체가 리턴된다. 그런데 위에서 우리는 catch 문으로 에러를 처리해주니, undefined 값이 담긴 fulfilled 상태의 promise 객체가 리턴될 것임.

필요에 따라서 async function에 then 메소드를 붙여서 처리하면 된다.

참고로 async 함수는 다 좋은데, 주의할 게 있다!!!

여러 개의 비동기문을 순차적으로 처리해야 한다면 그냥 알아서 await 키워드 각각 붙여서 실행하면 되지만...

async function getResponses(urls) {
  for(const url of urls){
    const response = await fetch(url);
    console.log(await response.text());
  }
}

만약 for 문 안의 response들이 순차적으로 말고 그냥 마구잡이로 와도 되는 것들이라면?
이렇게 쓰는 게 더 효율적이다.

async function fetchUrls(urls){
  for(const url of urls){
    (async () => { // 추가된 부분!
      const response = await fetch(url);
      console.log(await response.text());
    })(); // 추가된 부분!
  }
}

즉, 순차적인 처리가 아니고 그냥 비동기문 여러 개를 마구잡이로 받아올 때면 async 함수로 다시 묶어주자.