티스토리 뷰

개발공부/🟦 React.js

[React] 비동기 통신과 Promise

2022. 8. 16. 17:39
 

React – A JavaScript library for building user interfaces

A JavaScript library for building user interfaces

reactjs.org

 

 

 

 자바스크립트의 비동기 

 

 

▶  자바스크립트 비동기의 등장

 

초기 웹 환경에서는 서버에서 모든 데이터를 로드하여 페이지를 빌드했으므로

자바스크립트에는 별도의 비동기 처리가 필요하지 않았다.

 

Ajax (Asynchronous JavaScript and XML) 기술의 등장으로

페이지 로드 없이 client-side에서 서버로 요청을 보내 데이터를 처리할 수 있게 되었다.

 

XMLHttpRequest라는 객체를 이용해 서버로 요청을 보낼 수 있게 되었다.

 

 

 

 

 

▶  자바스크립트와 비동기

 

자바스크립트는 single-threaded language이므로

만일 서버 요청을 기다려야 한다면 유저는 멈춰있는 브라우저를 보게 될 수 있다.

 

→  따라서 동기가 아닌 비동기 처리를 이용해 서버로 통신할 필요가 있다.

 

 

-  비동기 요청 후 main thread는 유저의 입력을 받거나 페이지를 그리는 등의 작업을 처리한다.

-  비동기 응답을 받으면 응답을 처리하는 callback 함수를 task queue에 넣는다.

-  event loop는 main thread에 여유가 있을 때 task queue에서 함수를 꺼내 실행한다.

 

 

 

 

 

 

▶  동기와 비동기

 

동기(synchronous) 코드는 해당 코드 블록을 실행할 때 thread의 제어권을 넘기지 않고 순서대로 실행하는 것을 의미한다.

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!");


// This is synchronous...
// I am blocking the main thread...
// This is synchronous...DONE!

 

 

비동기(asynchronous) 코드는 코드의 순서와 다르게 실행된다.

 

-  비동기 처리 코드를 감싼 블록은 task queue에 넣어진다.

-  main thread가 동기 코드를 실행한 후에 제어권이 돌아왔을 때 event loop가 task queue에 넣어진 비동기 코드를 실행한다.

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...");
}

// This is synchronous...
// I am blocking the main thread...
// This is asynchronous...

 

 

 

 

 

 

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

 

브라우저에서 실행되는 자바스크립트 코드는 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("유저 입력");

이미지출처 : 엘리스 React 심화 Ⅰ - 03 비동기통신과 Promise 강의자료
이미지출처 : 엘리스 React 심화 Ⅰ - 03 비동기통신과 Promise 강의자료
이미지출처 : 엘리스 React 심화 Ⅰ - 03 비동기통신과 Promise 강의자료

 

 

 

 

 

 

 

 Promise 

 

 

▶  Callback pattern  vs.  Promise

 

 

Callback pattern

 

:  비동기 처리 후 실행될 코드를 Callback function으로 보내는 방식

 

→  비동기 처리가 고도화되면서 Callback hell 등이 단점으로 부각되었다.

→  Promise를 활용하여 비동기 처리의 순서 조작, 에러 핸들링, 여러 비동기 요청 처리 등을 쉽게 처리할 수 있다.

 

 

 

비교 1)  Single request

 

Callback pattern

function fetchUsers(onSuccess) {
	request('/users', onSuccess);
}

 

Promise 

function fetchUsers(onSuccess) {
	return request('/users').then(onSuccess);
}

 

 

 

 

비교 2)  Error handling

 

Promise 

function fetchUsers(onSuccess, onError) {
    return request('/users')
    	.then(onSuccess)
    	.catch(onError);
    
    // or
    return request('/users').then(onSuccess, onError);
}

 

 

 

비교3)  Multiple request

 

Callback pattern

function fetchUserAddress(onSuccess) {
    request('/users', (userData) => {
        const userDataWithAddress = [];
        const userLength = userData.length;
        
    	userData.map(user => request(`/users/${user.userId}/address`, (address) => {
    		const userDataWithAddress.push({ ...user, address });
            
    		if (userDataWithAddress.length === userLength) {
    			onSuccess(userDataWithAddress)
    		}
    	});
    });
}

 

Promise

function fetchUserAddress() {
	return request("/users").then((userData) =>
		Promise.all(
			userData.map((user) => request(`/users/{user.userId}/address`)
            					.then((address) => ({...user,address,}));
			);
		);
	);
}

 

 

 

 

 

 

▶  Promise

 

:  Promise 객체는 객체가 생성 당시에는 알려지지 않은 데이터에 대한 Proxy

 

-  비동기 실행이 완료된 후 `.then`, `.catch`, `.finally` 등의 핸들러를 붙여 각각 데이터 처리 로직, 에러 처리 로직, 클린업 로직을 실행한다.

-  then 체인을 붙여 비동기 실행을 마치 동기 실행처럼 동작하도록 할 수 있다.

-  then, catch는 비동기(Promise), 동기 실행 중 어떤 것이라도 리턴할 수 있다.

 

function returnPromise() {
    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개의 상태를 가진다.

 

pending   비동기 실행이 끝나기를 기다리는 상태
fulfilled   비동기 실행이 성공한 상태
rejected   비동기 실행이 실패한 상태

 

→  fulfilled, rejected 두 상태를 settled 라고 지칭한다.

 

 

 

[참고]  Promises/A+

 

Promises/A+

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. Th

promisesaplus.com

 

 

 

 

 

▶  Multiple Promise handling

 

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))
        .then((results) => {
        	return results.filter((result) => result.status === "rejected");
        })
        .then((failedPromises) => {
            saveLogRetry(
                failedPromises.map((promise) => promise.reason.failedLog),
                retryNum + 1
            );
	});
}

 

 

Promise.race() 

// wait()은 timeout만큼의 시간이 지나면 reject하는 함수
// const wait = (timeout) => 
//	 new Promise((resolve, reject) => {setTimeout(reject, timeout)});

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("아무것도 가져오지 못했습니다.");
        });
}

 

 

-  Promise객체는 settled되더라도 계속 핸들러를 붙일 수 있다.

-  핸들러를 붙인 순서대로 호출된다.

-  catch 뒤에 계속 핸들러가 붙어있다면 에러를 처리한 후에 계속 진행된다. (이때는 catch에서 리턴한 값이 then으로 전달됨)

Promise.resolve()
	.then(() => wait2(500).then(() => console.log("500 waited.")))
	.then(() => {
        	console.log("After 500 wait.")
        	return wait2(1000).then(() => console.log("1000 waited."))
	})
	.then(() => console.log("DONE"));
    
/*
500 waited.
After 500 wait.
1000 waited.
DONE
*/

 

 

 

 

 

 

 

 async / await 

 

 

▶  async/await 란?

 

-  Promise 체인을 구축하지 않고도 Promise를 직관적으로 사용할 수 있는 문법

-  많은 프로그래밍 언어에 있는 try ... catch 문으로 에러를 직관적으로 처리가 가능하다.

-  async function을 만들고 Promise를 기다려야 하는 표현 앞에 await을 붙인다.

// async/await
async function fetchUsers() {
    try {
        const users = await request('/users');
        console.log("users fetched.");
        return users;
    } catch (e) {
    	console.log("error : ", e);
    }
}


// Promise
function fetchUsers() {
    return request('/users')
        .then(users => console.log("users fetched."))
        .catch(e => console.error("error : ", e));
}

 

 

-  여러 개의 await을 순서대로 나열하여 then chain을 구현할 수 있다.

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);
    }
}

 

 

-  try구문 안에서 throw new Error() 로 중간에 에러를 던질 수 있다. (catch절로 넘어감)

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);
    }
}

 

 

 

-  try-catch안에 try-catch를 nesting할 수 있다.

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

 

 

-  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));

 

 

 

 

 


 이 글은 엘리스의 AI트랙 5기 강의를 들으며 정리한 내용입니다.

반응형

'개발공부 > 🟦 React.js' 카테고리의 다른 글

[React] 상태 관리  (0) 2022.08.19
[React] POSTMAN, OpenAPI, CORS  (0) 2022.08.16
[React] SPA와 라우팅  (0) 2022.08.13
[React] React 스타일링  (0) 2022.08.12
[React] Todo-List 만들기  (0) 2022.08.12
프로필사진
개발자 삐롱히

프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그