이 글은 김민준(velopert)님의 리액트를 다루는 기술을 참조하였습니다.
목차
1. 비동기 작업
2. Callback Function
3. Promise Object
4. async/await keyword
5. axios로 API 호출
6. 참고 자료
1. 비동기 작업
synchronous(동기)란 요청과 응답을 받는 시기가 일치한다는 것을 말한다.
하지만 인터넷을 하면서 언제나 서버에 요청한 시간과 응답을 받는 시간이 일치했는가? 아니다.
요청을 처리하는 속도는 아무도 모른다. (종종 서버에 얼마나 연결되었는지 퍼센트로 로딩화면을 표시하는 곳이 있는데, 그냥 심리적인 효과 때문에 넣은 장치고 아무 의미가 없다. 계산할 수 없는 내용이기 때문.)
그럼 그동안 어플리케이션이 잠시 멈추게 되는데, 사용자 입장에선 유쾌하지가 않다.
그래서 asynchronous(비동기)가 필요한 것이다.
비동기 작업을 간략하게 설명하면 서버에 요청한 데이터가 언제 처리될 지 모르니까 마냥 기다리고 있지 말고
그 동안 처리할 수 있는 일을 해놓는 것을 의미한다.
요청한 내용을 언젠가 응답해 주겠다고 약속을 해놓은 상태에서 그럼 다른 할 일을 먼저 해치우자는 것이다.
동기적(synchronously)으로 처리하면 요청이 끝날 때까지 다른 작업을 수행하지 못 한다.
하지만 비동기(asynchronously) 작업을 하면 웹 애플리케이션은 멈추지 않고 그동안 다른 여러 요청들을 처리할 수 있게 된다.
좀 더 그럴듯하게 설명하면 작업(Task)들이 순차적으로 이루어지지 않는 것이다.
그렇다면 비동기 작업은 어떻게 다룰 수 있을까? 구글링해보면 대표적으로 3가지가 있다고 하는데
- 콜백 함수
- Promise 객체
- async / await 키워드
처음에 내가 이 부분을 잘못 이해하고 넘어가서 공부할 때 어려움을 겪었는데 3가지 방법이 있다기 보다는
점점 비동기 작업이 보완되는 과정이라고 보는 게 맞다.
여기서 하나만 알고 가자. 콜백은 함수고, Promise는 객체고, async/await는 키워드다!!!!!!
2. Callback Function
function printMe() {
console.log("Hello World!");
}
setTimeout(printMe, 3000);
console.log("대기 중...");
Callback 함수는 이름 그대로 나중에 호출하는 함수를 말한다.
뭔가 거창해보이는 이름을 가지고 있지만 위의 예시를 보면 printMe라는 함수는 setTimeout이라는 함수의 인자로써 쓰여지고 있다.
그리고 어떤 함수에 의해 리턴되는 경우도 있을 수 있는데, 이런 함수를 '고차 함수'라고 한다.
여기서 인자로 넘겨지는 함수를 바로 콜백 함수라고 하는데 위에 내용 다 잊어버리고 다음 한 줄만 기억하면 된다.
함수 등록해놨다가 어떤 이벤트가 발생하거나 특정 시점에서 호출되는 함수가 콜백함수다.
대표적으로 이벤트 핸들러가 있다. (한 줄이 늘어버렸다 ^^)
Callback 함수의 단점이라고 한다면 콜백 지옥이라는 좀 웃긴 이름의 형태가 존재한다. (사실 안 웃기다...ㅠ)
step1(function (value1) {
step2(function (value2) {
step3(function (value3) {
step4(function (value4) {
step5(function (value5) {
step6(function (value6) {
// Do something with value6
});
});
});
});
});
});
여기까지만 보면 어떤 바보가 이런 식으로 코드를 짜겠냐 싶겠지만, 이벤트 처리나 서버 통신 같은 비동기적 작업을 위해 코드를 짜다보면 그 바보가 어느샌가 내가 되어 있다.
애초에 콜백 함수로 비동기를 제어한다는 건 콜백 함수 내부에서 (비동기 함수 호출 같은)다음 작업을 수행하는 건데
3번만 반복해도 콜백 지옥 비스무리한 것이 연출된다. 지옥은 생각보다 우리의 삶과 밀접해있다..
그보다 아까부터 setTimeout이란 함수는 뭔데 자꾸 친한 척하는 건가 싶어서 찾아봤더니
자바 스크립트의 대표적인 내장 비동기 함수였다. ㅎㅎ..
하지만 그렇다고 setTimeout 함수로 비동기를 다루는 것은 아니고 이 함수는 상황 시뮬레이션으로 자주 쓴다.
궁금증을 해결했으니 콜백 지옥을 빠져나갈 방법에 대해서 알아보자!
첫 번째는 Promise 기능을 사용하는 것이다.
3. Promise
1. Promise는 왜 나오게 되었나?
첫 째, Callback 함수만으로 비동기를 처리하는 데서 콜백헬이 발생한 이유는 함수에서 처리된 결과값을 반환할 경우, 그 결과값을 (비동기)함수 내에서 다시 찾아내야 한다. 그 과정에서 지옥이 연출되는 것이다.
둘 째, 콜백 패턴이 처리 순서를 100% 보장한다고 말할 수 없다. 그야 말로 실행 결과를 신만이 알게 된다.
셋 째, 에러 처리에 한계가 발생한다.
콜백 함수만으로 구현한 비동기 작업의 구조는 함수가 함수를 호출한다.
비동기 처리 함수가 콜백함수를 호출하고, 작업이 완료되기 전에 비동기 처리 함수는 호출 스택에서 빠진다.
이렇게 되면 콜백 함수를 호출한 호출자가 사라져버려서 비동기 처리 함수에서 에러가 발생해도
catch 연산자가 에러를 잡아내지 못 해서 최악의 경우 프로세스가 종료되어버릴 수 있다.
2. Promise의 특징
Promise는 JS 비동기 처리에 활용되는 객체로 생성부터 종료까지 3가지 State(process)를 갖는다.
상태 | 설명 |
Pending(대기) | 비동기 로직 처리의 미완료 상태 |
Fulfilled(이행) | 비동기 로직 처리가 완료된 상태. Promise 결과값 반환 |
Rejected(실패) | 비동기 로직 작업 실패 또는 오류 |
- ES6에 도입된 기능
- Promise는 new 연산자로 호출하고 인자로 콜백을 받는다.
- Promise를 반환한다.
- Promise는 호출되면 실행되지만 콜백은 resolve, reject 둘 중 하나가 호출되기 전까지는 then, catch로 넘어가지 않음
- resolve, reject는 각각 성공 혹은 실패 결과 값을 나타낸다.
- then으로 작업을 이어나가려면 resolve() 함수를 호출한다.
- then에서 다음 then으로 데이터 절달을 위해서는 반드시 리턴값으로 전달해야 한다.
- 작업을 고의로 중단시키거나 Err 예외처리를 위해서 reject() 함수를 호출한다.
Pending(대기) 상태는 객체 생성 시에 발생한다.
new Promise((resolve, reject) => {});
Promise 객체를 이용해 콜백함수를 선언할 수 있다.
FulFilled(이행)상태는 콜백함수 인자인 resolve가 실행되면 된다. resolve가 실행되면 사실상 완료된 상태다.
function increase(number) {
promise = Promise((resolve, reject) => {
const result = number + 10
resolve(result);
}
return promise
}
increase(0).then((number) => {
console.log(number);
// return increase(number); // 다음 then이 또 있다면 값을 넘김
}
.then을 이용해 결과값을 받을 수 있다.
then다음에 then이 계속 이어지게 된다면 return을 시켜주어야 한다.
Rejected(실패) 상태는 Promise 안에서 reject를 return시키면 된다.
function increase(number) {
promise = Promise((resolve, reject) => {
reject(new Error("에러"));
}
return promise
}
increase(0).catch((err) => {
console.log(err)
}
찾아보니 후속처리 메서드 중에 성공, 실패 여부 상관없이 호출되는 finally도 존재한다.
3. Promise 사용 예시
function increase(number, callback) {
setTimeout(() => {
const result = number + 10;
if (callback) {
callback(result);
}
}, 1000);
}
console.log("시작");
increase(0, result => {
console.log(result);
increase(result, result => {
console.log(result);
increase(result, result => {
console.log(result);
increase(result, result => {
console.log(result);
console.log("끝");
});
});
});
});
이런 정신 나간 코드가 있다고 생각해보자. 1초에 걸쳐 10, 20, 30, 40의 값을 순차적으로 처리해주는 내용이다.
이런 가독성이 떨어지는 코드를 Promise로 바꾸면 아래처럼 된다.
function increase(number, callback) {
const promise = new Promise((resolve, reject) => {
// resolve: 성공, reject: 실패
setTimeout(() => {
const result = number + 10;
if (result > 50) { // 50보다 커지면 에러 발생
const e = new Error("NumberTooBig");
return reject(e);
}
resolve(result);
}, 1000)
})
return promise;
}
increase(0)
.then(number => {
// Promise에서 resolve된 값은 .then으로 받아 올 수 있다.
console.log(number);
return increase(number); // Promise 리턴 시
})
.then(number => {
console.log(number);
return increase(number);
})
.then(number => {
console.log(number);
return increase(number);
})
.then(number => {
console.log(number);
return increase(number);
})
.then(number => {
console.log(number);
return increase(number);
})
.catch(e => { // Error 발생 시
console.log(e)
})
확실히 아까처럼 콜백 지옥이 발생하진 않았지만, 이것도 이거 나름대로..? 너무 길다.
그리고 잘 생각해보면 then() 계속해서 사용하면 또 다른 Promise 객체가 호출됨으로써 Promise Chain이 생긴다.
콜백 지옥보단 낫지만 최악에서 차악을 선택한 꼴이 되었다.
이런 Promise를 또 더 쉽고, 직관적으로 만들어주는 기능이 있다. 바로 async / await 키워드다.
4. async / await
async/await은 Promise를 더 쉽게 사용할 수 있게 해주는 문법이다.
실질적으로 비동기 작업이 필요한 함수 앞부분에 async 키워드를 추가하고,
해당 함수 내부의 Promise 앞부분에 await 키워드를 사용하면 된다.
이렇게 하면 Promise가 끝날 때까지 기다리고, 결과 값을 특정 변수에 담을 수 있다.
간단한 예시부터 먼저 확인해보자.
// Promise style
const users = () => {
getData()
.then(users => {
console.log(users);
return users;
})
.catch(error => {
console.log(error);
});
}
// async/await style
const users = async() => {
console.log(await getData());
return await getData();
}
async를 함수 앞에 붙이면 일반 함수처럼 값을 반환하는 게 아니라 Fulfiled Promise를 반환한다.
그리고 await을 붙인 Promise는 Promise를 리턴하지 않게 된다.
Promise에서 작성한 코드를 async/await 키워드를 사용하면 아래처럼 고칠 수 있다.
function increase(number, callback) {
const promise = new Promise((resolve, reject) => {
// resolve: 성공, reject: 실패
setTimeout(() => {
const result = number + 10;
if (result > 50) { // 50보다 커지면 에러 발생
const e = new Error("NumberTooBig");
return reject(e);
}
resolve(result);
}, 1000)
})
return promise;
}
async function runTasks() {
try {
let result = await increase(0);
while (true) {
console.log(result);
result = await increase(result);
}
} catch(e) {
console.log(e)
}
}
솔직히 이 코드들만 보고 이해하면 그게 대단한 거고, 실전으로 부딪혀봐야 아~ 이런 거구나 하고 이해할 수 있을 것 같다.
이론 백 날 공부해봐야 이해 못 한다.
개인적으로 이 블로그에 있는 예시가 너무 괜찮다고 느껴진다.
실제로 개발하면서 사용할 만한 코드를 예시로 설명해두었기 때문에, 더 와닿을 것이다.
다만 콜백 지옥의 예시로 주신 코드를 너무 깔끔하게 정리해놔서 콜백 지옥처럼 안 보인다. ㅋㅋㅋㅋ
5. axios로 API 호출
axios 라이브러리는 자바스크립트 HTTP 클라이언트로써 HTTP 요청을 Promise 기반으로 처리한다.
npm install axios을 bash에 입력하면 설치가 완료된다.
import './App.css';
import axios from 'axios';
import { useState } from 'react';
function App() {
const [data, setData] = useState(null);
const onClick = () => {
axios.get('https://jsonplaceholder.typicode.com/todos/1')
.then(response => {setData(response.data)});
};
return (
<div>
<div><button onClick={onClick}>불러오기</button></div>
{data && <textarea rows={7} value={JSON.stringify(data, null, 2)} readOnly={true} />}
</div>
);
}
export default App;
불러오기 버튼을 누르면 https://jsonplaceholder.typicode.com/todos/1에서 제공하는 가짜 API를 호출하여 컴포넌트 상태에 넣어 출력시켜준다.
함수에 인자로 던져준 주소에 GET 요청을 보내 얻어온 결과를 .then이 비동기적으로 확인시켜준다.
여기서 async를 적용하면 아래처럼 된다.
import './App.css';
import axios from 'axios';
import { useState } from 'react';
function App() {
const [data, setData] = useState(null);
const onClick = async() => {
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/todos/1',
);
setData(response.data)
} catch(e) {
console.log(e);
}
};
return (
<div>
<div><button onClick={onClick}>불러오기</button></div>
{data && <textarea rows={7} value={JSON.stringify(data, null, 2)} readOnly={true} />}
</div>
);
}
export default App;
진짜 댕어렵다.
실습 파트 코드를 올리면 뭔가 저작권에 걸릴 거 같은 쎄한 느낌이 드니까 포스팅은 여기까지.
6. 참고 자료