코드캠프

코드캠프 4일차

Grace 2022. 9. 29. 23:22

비동기 실행

fetch 함수와 비동기 실행

console.log('start!');

fetch('https://jsonplaceholder.typicode.com/users')
    .then((response) => response.text())
    .then((result) => { console.log(reuslt) });

console.log('End');

(1) (response) ⇒ response.text()
(2) (result) ⇒ { console.log(result); }

fetch 함수가 리퀘스트를 보내고, 서버의 리스폰스를 받게 되이 콜백들이 순서대로 실행되는데 이 사실을 바탕으로, 전체 코드의 실행 순서를 다시 정리하자면

  1. console.log('Start');
  2. fetch 함수(리퀘스트 보내기 및 콜백 등록)
  3. console.log('End');
  4. 리스폰스가 오면 2. 에서 then 메소드로 등록해뒀던 콜백 실행

이렇게 됩니다. 이렇게 특정 작업을 시작(리퀘스트 보내기)하고 완벽하게 다 처리(리스폰스를 받아서 처리)하기 전에, 실행 흐름이 바로 다음 코드로 넘어가고, 나중에 콜백이 실행되는 것을 '비동기 실행'이라고 한다. 이에 반해 한번 시작한 작업은 다 처리하고 나서야, 다음 코드로 넘어가는, 우리에게 익숙한 방식의 실행은 '동기 실행'이라고 한다. 만약 이 코드에서 fetch 함수가 비동기 실행되지 않고, 동기 실행되는 함수였다고 가정한다면

  1. console.log('Start');
  2. fetch 함수(리퀘스트 보내기)
  3. 리스폰스가 올 때까지 코드 실행이 잠시 '정지'되고, 리스폰스가 오면 필요한 처리 수행
  4. console.log('End');

이런 순서로 코드가 실행되었을 것이다. 동기 실행은 한번 시작한 작업을 완료하기 전까지 코드의 실행 흐름이 절대 그 다음 코드로 넘어가지 않는다. 일단 시작한 작업을 완벽하게 처리하고 난 다음에야 그 다음 코드로 실행 흐름이 넘어가기 때문에 동기 실행의 경우 코드가 보이는 순서대로, 실행된다.

이와 다르게 비동기 실행은 한번 작업을 시작해두고, 그 작업이 완료되기 전이더라도 콜백만 등록해두고, 코드의 실행 흐름이 바로 그 다음 코드로 넘어간다. 그리고 추후에 특정 조건이 만족되면 콜백이 실행됨으로써 해당 작업을 완료하는 방식인 것이다. 코드가 꼭 등장하는 순서대로 실행되는 것은 아니기 때문에 비동기 실행에서는 코드를 해석할 때 주의해야 한다.

'비동기 실행'이 '동기 실행'에 비해, 동일한 작업을 더 빠른 시간 내에 처리할 수 있다. fetch 함수가 '동기 실행'된다고 가정했을 때 fetch 함수가 실행되고 리스폰스가 올 때까지 아무런 작업도 할 수 없지만 비동기 실행이라면 일단 리퀘스트 보내기, 콜백 등록까지만 해두고, 바로 다음 작업을 시작함으로써 시간을 절약할 수 있다.

이미지 상단은 fetch 함수가 동기 실행된다고 가정했을 때의 경우,
이미지 하단은 fetch 함수가 비동기 실행되는 실제의 모습을 의미한다.
동기 실행에서는 모든 작업이 순차적으로 수행되고 있는 데 비해 비동기 실행에서는 리스폰스를 기다리는 시간 동안 그 이후의 작업을 미리 처리하고 있다. 그래서 비동기 실행이 최종 작업 종료 시간이 더 짧다는 것을 알 수 있다.

자바스크립트로 웹 통신을 하는 코드를 작성하려면 이런 비동기 실행의 원리와 그 장점에 대해 잘 이해하고 있어야 한다.

비동기 실행 함수

setTimeout 함수

특정 함수의 실행을 원하는 시간만큼 뒤로 미루기 위해 사용하는 함수.

console.log('a');
setTimeout(() => { console.log('b'); }, 2000);
console.log('c');

가운데에 있는 setTimeout 함수는 첫 번째 파라미터에 있는

() ⇒ { console.log('b'); },

이 콜백의 실행을, 두 번째 파라미터에 적힌 2000 밀리세컨즈(=2초) 뒤로 미룬다. 그래서 이 코드를 실행하면

이렇게 a와 c가 먼저 출력되고, 약 2초가 지난 후에 b가 출력된다.

fetch 함수에서는 콜백이 실행되는 조건이, '리스폰스가 도착했을 때'였다면, setTimeout에서 콜백이 실행되는 조건은, '설정한 밀리세컨즈만큼의 시간이 경과했을 때'이다.

setInterval 함수

특정 콜백을 일정한 시간 간격으로 실행하도록 등록하는 함수.

console.log('a');
setInterval(() => { console.log('b'); }, 2000);
console.log('c');

b를 출력하는 콜백이 2초 간격으로 계속 실행된다. 실제로 확인해보면

a와 c가 출력되고, 약 2초 뒤에 b가 출력된 후 그 뒤로 계속 2초 간격으로 b가 반복 출력되는 것을 볼 수 있다.

addEventListener 메소드

addEventListener 메소드는 DOM 객체의 메소드로 웹 페이지에서 어떤 버튼 등을 클릭했을 때, 실행하고 싶은 함수가 있다면

(1) 해당 DOM 객체의 onclick 속성에 그 함수를 설정하거나,
(2) 해당 DOM 객체의 addEventListener 메소드의 파라미터로 전달

onclick 속성

...

btn.onclick = function (e) {// 해당 이벤트 객체가 파라미터 e로 넘어옵니다.
console.log('Hello Codeit!');
};

// 또는 arrow function 형식으로 이렇게 나타낼 수도 있습니다.
btn.onclick = (e) => {
  console.log('Hello Codeit!');
};

...

addEventListener 메소드

...

btn.addEventListener('click', function (e) {// 해당 이벤트 객체가 파라미터 e로 넘어옵니다.
console.log('Hello Codeit!');
});

// 또는 arrow function 형식으로 이렇게 나타낼 수도 있습니다.
btn.addEventListener('click', (e) => {
  console.log('Hello Codeit!');
});

...

이렇게 클릭과 같은 특정 이벤트가 발생했을 때 실행할 콜백을 등록하는 addEventListener 메소드도 비동기 실행과 관련이 있다. 파라미터로 전달된 콜백이 당장 실행되는 것이 아니라, 나중에 특정 조건(클릭 이벤트 발생)이 만족될 때(마다) 실행되기 때문이다.

fetch 함수와 이 함수들을 보면 차이점이 있다.

setTimeout(콜백, 시간)
setInterval(콜백, 시간)
addEventListener(이벤트 이름, 콜백)

위의 함수들은 함수의 아규먼트로 바로 콜백을 넣는다. 그런데 fetch 함수는 이 함수들과는 전혀 다르게 생겼다.

fetch('https://www.google.com')
  .then((response) => response.text())// fetch 함수가 리턴하는 객체의 then 메소드를 사용해서 콜백을 등록
  .then((result) => { console.log(result); });

fetch 함수는 콜백을 파라미터로 바로 전달받는 게 아니라, fetch 함수가 리턴하는 어떤 객체의 then 메소드를 사용해서 콜백을 등록하는데, fetch 함수는 Promise 객체라는 것을 리턴하고, 이 Promise 객체는 비동기 실행을 지원하는 또 다른 종류의 문법에 해당한다

async/await

axios

fetch 함수는 Ajax 통신을 하는 함수이다. fetch 함수 말고도 Ajax 통신을 할 수 있는 방법은 axios라고 하는 외부 패키지를 사용하는 것이다.

axios
  .get('https://jsonplaceholder.typicode.com/users')
  .then((response) => {
    console.log(response);
  })
  .catch((error) => {
    console.log(error);
  });

이 코드는 axios 패키지에서 제공하는 axios 객체를 사용해서 GET 리퀘스트를 보내고 그 리스폰스를 받는 코드이다.
axios 객체에서 리퀘스트를 보내는 많은 메소드들이 Promise 객체를 리턴한다.

axios 객체에는 몇 가지 기능 및 장점들이 있다.

  • 모든 리퀘스트, 리스폰스에 대한 공통 설정 및 공통된 전처리 함수 삽입 가능
  • serialization, deserialization을 자동으로 수행
  • 특정 리퀘스트에 대해 얼마나 오랫동안 리스폰스가 오지 않으면 리퀘스트를 취소할지 설정 가능(request timeout)
  • 업로드 시 진행 상태 정보를 얻을 수 있음
  • 리퀘스트 취소 기능 지원

axios가 fetch에 비해 다양한 기능을 지원하는 것은 맞지만 단점도 있다. fetch 함수는 웹 브라우저에서 바로 지원되는 함수이기 때문에 별도로 패키지를 다운로드받지 않아도 되지만, axios는 별도로 패키지를 다운로드해줘야 한다.

그래서 axios에서 제공하는 추가 기능이 필요한 경우에는 axios를 쓰고, 그런 기능이 필요하지 않고 별도의 패키지 다운로드를 원하지 않는 경우에는 fetch 함수를 사용합니다.

실무에서는 fetch 이외에 axios도 많이 사용하고 있으며axios 또한 리퀘스트를 보내는 주요 메소드들이 Promise 객체를 리턴한다.

그럼 본격적으로 async/await 함수에 대해서 알아보자.

async function fetchAndPrint() {
    try {
        const response = await fetch('https://www.google.www');
    const result = await response.text();
    console.log(result);
    } catch (error) {
        console.log(error);
    } finally {
        console.log('exit');
    }
}

fetchAndPrint();

Promise 객체를 사용하는 코드(Promise Chaining)를

(1) 개발자가 더 편하게 작성할 수 있도록 하고
(2) 코드의 가독성을 높이기 위해서

도입된 일종의 Syntactic sugar(기존 문법을 더 편하게 사용할 수 있도록 하는 문법적 장치)

Promise 객체 return

async 함수는 리턴하는 값에 따라 Promise 객체를 리턴한다.

  • 어떤 값을 리턴하는 경우
  • Promise 객체를 리턴하는 경우*
    async 함수 안에서 Promise 객체를 리턴하는 경우에는 해당 Promise 객체와 동일한 상태와 작업 성공 결과(또는 작업 실패 정보)를 가진 Promise 객체를 리턴한다.
async function fetchAndPrint() {
  return new Promise((resolve, reject)=> {
    setTimeout(() => { resolve('abc'); }, 4000);
  });
}

fetchAndPrint();


이렇게 pending 상태의 Promise 객체를 리턴하기도 하고(리턴된 Promise 객체는 약 4초 후에 fulfilled 상태가 된다.

async function fetchAndPrint() {
  return Promise.resolve('Success');
}

fetchAndPrint();


이미 fulfilled 상태인 Promise 객체나

async function fetchAndPrint() {
  return Promise.reject(new Error('Fail'));
}

fetchAndPrint();

))
이미 rejected 상태인 Promise 객체를 리턴하는 경우 전부 다 해당한다. (위 이미지에서는 rejected 상태의 Promise 객체를 따로 처리해주지 않았기 때문에 에러 발생)

Promise 객체 이외의 값을 리턴하는 경우
async 함수 내부에서 Promise 객체 이외에 숫자나 문자열, 일반 객체 등을 리턴하는 경우에는, fulfilled 상태이면서, 리턴된 값을 작업 성공 결과로 가진 Promise 객체를 리턴한다.

async function fetchAndPrint() {
  return 3;
}

fetchAndPrint();


이런 코드나

async function fetchAndPrint() {
  return 'Hello';
}

fetchAndPrint();


이런 코드,

async function fetchAndPrint() {
  const member = {
    name: 'Jerry',
    email: 'jerry@codeitmall.kr',
    department: 'sales',
  };

  return member;
}

fetchAndPrint();


이런 코드들 모두 여기에 해당한다.

  • 아무 값도 리턴하지 않는 경우
    async function fetchAndPrint() {
    console.log('Hello Programming!');
    }
    

fetchAndPrint();

이렇게 함수에서 아무런 값도 리턴하지 않으면 undefined를 리턴한 것으로 간주하기 때문에
![](https://images.velog.io/images/nej1044/post/88bd6aaf-64ae-4b34-90a1-2afd5eb25cbc/Untitled%206.png)
이 경우에는 **fulfilled 상태이면서, undefined를 작업 성공 결과로 가진 Promise 객체**가 리턴된다.
- **async 함수 내부에서 에러가 발생했을 때**
```jsx
async function fetchAndPrint() {
  throw new Error('Fail');
}

fetchAndPrint();


async 함수 안에서 에러가 발생하면, rejected 상태이면서, 해당 에러 객체를 작업 실패 정보로 가진 Promise 객체가 리턴된다.

두 가지 종류의 콜백

자바스크립트에서 콜백은 어떤 함수의 파라미터로 전달되는 모든 함수를 의미하는 개념이다. 그러니까 어떤 함수의 파라미터로 전달되기만 한다면 해당 함수는 그 함수의 콜백이 되는 것이다. 이런 콜백은
1. 동기 실행되는 콜백
2. 비동기 실행되는 콜백
으로 나뉜다.

형식이 어찌되었든 함수 안에 들어간 함수는 모두 콜백이다.

apollo client

아폴로 클라이언트의 async/await 명령어를 사용하는 방법이다.

apollo client 초기 설정해주기

apollo client를 사용하기 위해서는 설정 페이지인 app.js에서 apollo client를 설정해주어야한다.

// 설정파일
import '../styles/globals.css'
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'

function MyApp({ Component, pageProps }) {
  const client = new ApolloClient({
    uri: 'http://example.codebootcamp.co.kr/graphql',
    cache: new InMemoryCache()
  })

  return(
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  )                                          

}

export default MyApp

useMutation 활용하기

apollo client 설정이 완료되었다면 useMutation을 활용하여 data를 전송해줄 수 있다.
다음은 apollo client 형식으로 async/await 명령어를 사용해준 코드이다.

import { useMutation, gql } from '@apollo/client'
import { useState } from 'react'

const CREATE_BOARD = gql`
  mutation {
    createBoard(writer: "노은정", title: "게시글이에용", contents: "내용이구용") {
    _id
    number
    message
    }
  }
`

export default function GraphglMutationBoard1Page() {
  // 구조분해할당/비구조할당
  const [ createBoard ] = useMutation(CREATE_BOARD)
  const [message, setMessage] = useState('')
  // 함수
  async function zzz() {
    const result = await createBoard()
    console.log(result)
    console.log(result.data.createBoard.message)
    setMessage(result.data.createBoard.message)
  }


  return (
    <>
      <div>{message}</div>
      <button onClick={zzz}>GRAPHQL-API 요청하기</button>
    </>
  )
}

현재는 하드코딩 방식을 통해 게시글을 전송해주었다.
다음 코드는 이것을 각각 state로 변경하여 mutation request를 한 코드이다.

import { useMutation, gql } from '@apollo/client'
import { useState } from 'react'

const CREATE_BOARD = gql`
  mutation createBoard($writer: String, $title: String, $contents: String) {
    createBoard(writer: $writer, title: $title, contents: $contents) {
    _id
    number
    message
    }
  }
`


export default function GraphglMutationBoard3Page() {
  // 구조분해할당/비구조할당
  const [ createBoard ] = useMutation(CREATE_BOARD)
  const [myWriter, setMyWriter] = useState("")
  const [myTitle, setMyTitle] = useState("")
  const [myContents, setMyContents] = useState("")

  function onChangeMyWriter(event) {
    setMyWriter(event.target.value)
  }

  function onChangeMyTitle(event) {
    setMyTitle(event.target.value)
  }

  function onChangeMyContents(event) {
    setMyContents(event.target.value)
  }

  // 함수
  async function zzz() {
    const result = await createBoard( {
      variables: {writer: myWriter, title: myTitle, contents: myContents}
    })
    console.log(result)
    console.log(result.data.createBoard.message)
  }


  return (
    <>
      작성자: <input type="text" onChange={onChangeMyWriter} /><br />
      제목: <input type="text" onChange={onChangeMyTitle} /><br />
      내용: <input type="text" onChange={onChangeMyContents} /><br />
      <button onClick={zzz}>게시물 등록</button>
    </>
  )
}

'코드캠프' 카테고리의 다른 글

코드캠프 6일차  (0) 2022.09.30
코드캠프 5일차  (0) 2022.09.29
코드캠프 3일차  (0) 2022.09.29
코드캠프 2일차  (0) 2022.09.29
코드캠프 1일차  (0) 2022.09.29