[TIL] 엘리스 SW 엔지니어 트랙 Day 056
글 작성자: 망고좋아
반응형
📖 오늘 배운 내용 - 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를 종류별로 관리할 수 있다.
- 환경 변수를 정의하여, 환경별로 테스트 가능하다.
📝 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을 구현하는 코드는 참신했다. 아이디어가 참 좋다....!
📌 참고
반응형
'프로그래밍 > Today I Learned' 카테고리의 다른 글
[TIL] 엘리스 SW 엔지니어 트랙 Day 058 (0) | 2022.01.16 |
---|---|
[TIL] 엘리스 SW 엔지니어 트랙 Day 057 (0) | 2022.01.16 |
[TIL] 엘리스 SW 엔지니어 트랙 Day 055 (0) | 2022.01.13 |
[TIL] 엘리스 SW 엔지니어 트랙 Day 054 (0) | 2022.01.13 |
[TIL] 엘리스 SW 엔지니어 트랙 Day 053 (0) | 2022.01.11 |
댓글
이 글 공유하기
다른 글
-
[TIL] 엘리스 SW 엔지니어 트랙 Day 058
[TIL] 엘리스 SW 엔지니어 트랙 Day 058
2022.01.16 -
[TIL] 엘리스 SW 엔지니어 트랙 Day 057
[TIL] 엘리스 SW 엔지니어 트랙 Day 057
2022.01.16 -
[TIL] 엘리스 SW 엔지니어 트랙 Day 055
[TIL] 엘리스 SW 엔지니어 트랙 Day 055
2022.01.13 -
[TIL] 엘리스 SW 엔지니어 트랙 Day 054
[TIL] 엘리스 SW 엔지니어 트랙 Day 054
2022.01.13