글 작성자: 망고좋아
반응형

📖 오늘 배운 내용 - 2022.01.08

  • 자바스크립트 비동기
  • Promise
  • CORS(Cross-Origin Resource Sharing)

 

📝 동기 vs 비동기

📕 동기(synchronous)

console.log("This is synchronous...")

for (let i = 0; i < 1000000000; ++i) {
    console.log("I am blocking the main thread...")
}

console.log("This is synchronous...DONE!")
  • 동기(synchronous) 코드는, 해당 코드 블록을 실행할 때 thread의 제어권을 넘기지 않고 순서대로 실행하는 것을 의미한다.
  • 동기 코드는 바로 call stack에 넣어진다.
  • 동기 코드는 main thread에 의해 실행되므로, 무한루프 등에 의해 main thread를 블록 할 수 있다.

 

📕 비동기(asynchronous)

setTimeout(() => console.log("This is asynchronous..."), 5000)

console.log("This is synchronous...")

for (let i = 0; i < 1000000000; ++i) {
    console.log("I am blocking the main thread...")
}

/* result
This is synchronous...
I am blocking the main thread...
...
I am blocking the main thread...
This is asynchronous...
*/
request("user-data", (userData) => {
    console.log("userData 로드")
    saveUsers(userData)
});

console.log("DOM 변경")
console.log("유저 입력")
  • 비동기(asynchronous) 코드는, 코드의 순서와 다르게 실행된다.
  • 비동기 처리 코드를 감싼 블록은 task queue에 넣어진다.
  • main thread가 동기 코드를 실행한 후에 제어권이 돌아왔을 때 event loop가 task queue에 넣어진 비동기 코드를 실행한다.

 

📝 비동기 처리를 위한 내부 구조

  • 브라우저에서 실행되는 자바스크립트 코드는 event driven 시스템으로 작동한다.
    • 요약하자면 전체 시스템이 이벤트가 들어오고 이벤트를 처리하는 방식으로 처리하는 방식으로 작동한다.
  • 웹앱을 로드하면 브라우저는 HTML document를 읽어 문서에 있는 CSS code, JS code를 불러온다.
  • 자바스크립트 엔진은 코드를 읽어 실행한다.
  • 브라우저의 main thread는 자바스크립트 코드에서 동기적으로 처리되어야 할 코드 실행 외에도, 웹 페이지를 실시간으로 렌더링 하고, 유저의 입력을 감지하고, 네트워크 통신을 처리하는 등 수많은 일을 처리한다.
  • 비동기 작업을 할당하면, 비동기 처리가 끝나고 브라우저는 task queue에 실행코드를 넣는다
  • main thread는 event loop를 돌려, task queue에 작업이 있는지 체크한다.
  • 작업이 있으면 task를 실행한다.
request("user-data", (userData) => {
    console.log("userData 로드")
    saveUsers(userData)
});

console.log("DOM 변경")
console.log("유저 입력")

 

📝 비동기 처리를 위한 내부 구조 - 다이어그램

  • task queue이란?
    • 비동기 처리가 발생했을 때 일들이 들어오는 queue
    • 즉 비동기 처리가 발생하면 task queue로 들어온다.
  • Event Loop란?
    • main thread에 여유가 생겼을 때 task queue에서 꺼내와서 call stack에서 넣는 역할을 하는 모듈이다.
  • call stack이란?
    • 함수를 호출했을 때 그 함수가 내부적으로 또 다른 함수를 호출하면 콜스택에 그 함수의 시작 주소가 계속 쌓인다.

 

📝 Promise

  • Promise 객체는, 객체가 생성 당시에는 알려지지 않은 데이터에 대한 Proxy다.
  • 비동기 실행이 완료된 후에, .then, .catch, .finally 등의 핸들러를 붙여 각각 데이터 처리 로직, 에러 처리 로직, 클린업 로직을 실행한다.
  • then 체인을 붙여, 비동기 실행을 마치 동기 실행처럼 동작하도록 해준다.
function returnPromise() {
    // promise 객체를 return 한다.
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const randomNumber = generateRandomNumber(100)
            if (randomNumber < 50) resolve(randomNumber)
            else reject(new Error("Random number is too small."))
        }, 1000)
    })
}
returnPromise()
    .then(num => {
        console.log("First random number : ", num)
    })
    .catch(error => {
        console.error("Error occured : ", error)
    })
    .finally(() => {
        console.log("Promise returned.")
})
  • Promise 객체는 pending, fulfilled, rejected 3개의 상태를 가진다.
  • fulfilled, rejected 두 상태를 settled라고 지칭한다.
  • pending : 비동기 실행이 끝나기를 기다리는 상태
  • fulfilled : 비동기 실행이 성공한 상태
  • rejected : 비동기 실행이 실패한 상태
  • then, catch는 비동기(Promise), 동기 실행 중 어떤 것이라도 리턴할 수 있다.

 

📝 Multiple Promise handling(Promise.all, Promise.allSettled, Promise.race, Promise.any)

  • Promise.all()은 모든 프로미스가 fulfilled 되길 기다린다. 하나라도 에러 발생 시, 모든 프로미스 요청이 중단된다.
  • Promise.allSettled() : 모든 프로미스가 settled 되길 기다린다.
  • Promise.race() : 넘겨진 프로미스들 중 하나라도 settled 되길 기다린다.
  • Promise.any() : 넘겨진 프로미스 중 하나라도 fulfilled 되길 기다린다.

 

📕 Promise.all

Promise.all(
    users.map(user => request('/users/detail', user.name))
    // [Promise, Promise, ... ,Promise ]
)
    .then(console.log) // [UserNameData, UserNameData, ..., UserNameData]
    .catch(e => console.error("하나라도 실패했습니다.")

 

📕 Promise.allSettled

function saveLogRetry(logs, retryNum) {
    if (retryNum >= 3) return; // no more try.

    Promise.allSettled(logs.map(saveLog)) // saveLog("log a"), saveLog("log b") 이렇게 호출
        .then((results) => {
            return results.filter((result) => result.status === "rejected");
        })
        .then((failedPromises) => { // 위에서 return된 것을 여기서 받는다.
            saveLogRetry( // 재귀
                failedPromises.map((promise) => promise.reason.failedLog),
                retryNum + 1
        );
    });
}

 

📕 Promise.race

function requestWithTimeout(request, timeout = 1000) {
    return Promise.race([request, wait(timeout)]).then((data) => {
        console.log("요청 성공.");
        return data;
    });
}

requestWithTimeout(req)
    .then(data => console.log("data : ", data))
    .catch(() => console.log("타임아웃 에러!"))

 

📕 Promise.any

function getAnyData(dataList) {
    Promise.any(dataList.map((data) => request(data.url)))
        .then((data) => {
            console.log("가장 첫 번째로 가져온 데이터 : ", data);
    })
        .catch((e) => {
            console.log("아무것도 가져오지 못했습니다.");
    });
}

 

📝 async/await

  • Promise 체인을 구축하지 않고도, Promise를 직관적으로 사용할 수 있는 문법이다.
  • 많은 프로그래밍 언어에 있는 try... catch 문으로 에러를 직관적으로 처리한다.
  • async function을 만들고, Promise를 기다려야 하는 표현 앞에 await을 붙인다.

 

📕 async/await - 여러 개의 await

  • 여러 개의 await을 순서대로 나열하여, then chain을 구현할 수 있다.
  • try... catch 문을 자유롭게 활용하여 에러 처리를 적용할 수 있다.
async function fetchUserWithAddress(id) {
    try {
        const user = await request(`/user/${id}`)
        const address = await request(`/user/${user.id}/address`)
        return { ...user, address }
    } catch (e) {
        console.log("error : ", e)
    }
}

 

📕 try... catch를 여러 개 사용하여 에러 처리하는 방법

async function fetchUserWithAddress(id) {
    let user = null

    try {
        user = await request(`/user/${user.id}`)
    } catch (e) {
        console.log("User fetch error: ", e)
        return
    }

    try {
        const address = await
        request(`/user/${user.id}/address`)
        return { ...user, address }
    } catch (e) {
        console.log("Address fetch error: ", e)
    }
}

 

📕 throw new Error를 통해 중간에서 에러 처리

async function fetchUserWithAddress(id) {
    try {
        const user = await request(`/user/${user.id}`)
        if (!user) throw new Error("No user found.")

        const address = await request(`/user/${user.id}/address`)
        if (!address.userId !== user.id) throw new Error("No address match with user.")

        return { ...user, address }
    } catch (e) {
        console.log("User fetch error: ", e)
    }
}

 

📕 nested try-catch

async function fetchUserWithAddress(id) {
    try {
        const user = await request(`/user/${id}`)
        const address = await request(`/user/${user.id}/address`)
        return { ...user, address }
    } catch (e) {
        try {
            await sendErrorLog(e)
        } catch (e) {
            console.error("에러를 로깅하는데 실패하였습니다.")
        }
    }
}

 

📝 async/await - Promise 와의 조합

  • Promise.all은 특정 비동기 작업이 상대적으로 빠르게 끝나도 느린 처리를 끝까지 기다려야만 한다.
  • 이와 달리, async/await을 활용할 경우 parallelism을 구현할 수 있다.
  • 즉, 끝난 대로 먼저 처리될 수 있다.
async function fetchUserWithAddress(id) {
    return await Promise.all([
        (async () => await request(`/users/${id}`))(),
        (async () => await request(`/users/${id}/address`))(),
    ]);
}

fetchUserWithAddress('1234')
    .then(([user, address]) => ({ ...user, address }))
    .catch(e => console.log("Error : ", e))

 

📝 POSTMAN

  • POSTMAN은 API를 테스트하기 위한 개발 도구다.
  • Auth, header, payload, query 등 API 요청에 필요한 데이터를 쉽게 세팅할 수 있다.
  • 다른 개발자가 쉽게 셋업해 테스트할 수 있도록 API 정보를 공유할 수 있다.
  • Request를 모아 Collection으로 만들어, API를 종류별로 관리할 수 있다.
  • 환경 변수를 정의하여, 환경별로 테스트 가능하다.
 

Postman API Platform | Sign Up for Free

Postman is an API platform for building and using APIs. Postman simplifies each step of the API lifecycle and streamlines collaboration so you can create better APIs—faster.

www.postman.com

 

📝 Open API

  • RESTful API를 하나의 문서로 정의하기 위한 문서 표준이다.
  • OpenAPI Specification(OAS)으로 정의된다.
  • Swagger 등의 툴로, Open API로 작성된 문서를 파싱 해 테스팅 도구로 만들 수 있다.
  • 프론트엔드 개발자, 백엔드 개발자와의 협업 시 주요한 도구로 사용된다.
  • API의 method, endpoint를 정의할 수 있다.
  • endpoint마다 인증 방식, content type 등 정의할 수 있다.
  • body data, query string, path variable 등 정의할 수 있다.
  • 요청, 응답 데이터 형식과 타입 정의 - data model 활용(schema)

 

📝 CORS(Cross-Origin Resource Sharing)

  • 브라우저는 모든 요청 시 Origin 헤더를 포함한다.
  • 서버는 Origin 헤더를 보고, 해당 요청이 원하는 도메인에서부터 출발한 것인지를 판단한다.
  • 다른 Origin에서 온 요청은 서버에서 기본적으로 거부한다.
  • 그러나, 보통 서버의 endpoint와 홈페이지 domain은 다른 경우가 많음.
  • 따라서 서버에서는 홈페이지 domain을 허용하여, 다른 domain이라 하더라도 요청을 보낼 수 있게 한다.
  • 서버는 Access-Control-Allow-Origin 외에 Access-Control-* 을 포함하는 헤더에 CORS 관련 정보를 클라이언트로 보낸다.
    • 브라우저는 다른 Origin의 서버에 대해 Access-Control-* 헤더를 요구하고, 서버는 기본적으로 해당 헤더를 보내지 않는다. 허용하려면 별도로 cors 설정을 해주어야 한다.
  • 웹사이트에 악성 script가 로드되어, 수상한 요청을 하는 것을 막기 위함이다.
  • 반대로, 익명 유저로부터의 DDos 공격 등을 막기 위함이다.
  • 서버에 직접 CORS 설정을 할 수 없다면, Proxy 서버 등을 만들어 해결한다.

 

📝 리액트에서 API 사용 방법

useEffect(() => {
    async function fetchUser() {
        // try ~ catch를 이용해 예외 처리를 하세요.
        try {
            const response = await axios.get(
                '<https://jsonplaceholder.typicode.com/error>'
            );
            setUsers(response.data);
        } catch (e) {
            setError(e);
        }
    };
    fetchUser();
}, []);

 

📝 axios cancelToken

useEffect(() => {
  setLoading(true)
  let cancel;

  axios.get(currentPageUrl, { 
    cancelToken: new axios.CancelToken(c => cancel = c)
  }).then(res => {
    setLoading(false)
    setNextPageUrl(res.data.next)
    setPrevPageUrl(res.data.previous)
    setPokemon(res.data.results.map(p => p.name))
  })

  return () => cancel()
}, [currentPageUrl])
  • axios.get(){ cancelToken: new axios.CancelToken(c => cancel = c) }을 함께 넘겨줌으로써 페이지에 대한 정보가 바뀌는 것을 정리할 수 있다.
  • cancelToken은 Axios에서 제공하는 것으로 API 요청을 취소하는 데 사용되는 토큰이다.

 

📝useReducer로 API 다루기

import React, { useEffect, useReducer } from 'react';
import axios from 'axios';

function reducer(state, action) {
    switch (action.type) {
        case 'LOADING':
            return {
                loading: true,
                data: [],
                error: null
            };
        case 'SUCCESS':
            return {
                loading: false,
                data: action.data,
                error: null
            };
        case 'FAIL':
            return {
                loading: false,
                data: [],
                error: action.error
            };
        default:
            throw new Error();
    }
}

function Users() {
    const [state, dispatch] = useReducer(reducer, {
        loading: false,
        data: [],
        error: null

    });

    async function fetchUser() {
        try {
            dispatch({ type: 'LOADING' });
            const response = await axios.get(
                '<https://jsonplaceholder.typicode.com/users>'
            );
            dispatch({ type: 'SUCCESS', data: response.data });
        } catch (e) {
            dispatch({ type: 'FAIL', error: e });
        }
    };

    useEffect(() => {
        fetchUser();
    }, []);

    const { loading, data, error } = state;

    if(loading)
        return <h4>로딩중...</h4>;
    if(error)
        return <h4>에러 발생!</h4>;

    const userName = data.map(
        (user) => (<li key={user.id}> {user.name} </li>)
    );

    return (
        <>
            <h4>사용자 리스트</h4>
            <div> {userName} </div>
            <button onClick={fetchUser}>다시 불러오기</button>
        </>
    );
}

export default Users;

 

💡 오늘 깨달은 것

  • async/await - Promise를 통해 parallelism을 구현하는 코드는 참신했다. 아이디어가 참 좋다....!

 

📌 참고

반응형