통신을 할 때 promise로 접근해, try-catch로 오류를 처리하곤 한다.
동기식 언어인 javascript가 어떻게 비동기적인 동작을 수행하고, 에러를 처리하는 원리가 무엇인지 자세히 정리를 해보았다.
또한 promise가 탄생한 배경과, 비동기처리에 자주 쓰이는 콜백함수까지 이해해보는 시간을 가져보도록 하자.
아래의 코드 출처는 모던 자바스크립트이다.
1-1. try-catch로 에러를 잡자 (10.1)
Try-catch 구문을 이용해서 try를 통해 구문을 실행하고, catch를 통해 에러를 잡았을 것이다.
아래와 같이 JSON형태에서 에러를 잡는 것이 예시가 될 수 있다.
let json = "{ wrong json }";
try {
let user = JSON.parse(json); // <-- 여기서 에러가 발생하므로
alert( user.name ); // 이 코드는 동작하지 않는다.
} catch (e) {
// 에러가 발생하면 제어 흐름이 catch 문으로 넘어온다.
alert( "데이터에 에러가 있어 재요청을 시도한다." );
alert( e.name ); //에러의 이름
alert( e.message ); //에러 상세 내용을 담은 메세지
}
그런데 만약, JSON의 parsing은 성공적으로 이루어진다는 가정 아래,
json파일의 잘못된 프로퍼티를 접근했다면 어떻게 에러 처리를 할 수 있을까?
1-2. throw로 원하는 에러를 넘기자
우리는 throw 연산자를 통해 우리가 찾고자 하는 에러를 생성할 수 있다.
throw 연산자를 통해 우리는 다양한 에러 객체를 처리해줄 수 있다.
new 에러name(에러message);
// or
let error = new Error(message);
let error = new SyntaxError(message);
let error = new ReferenceError(message);
아래는 직접 throw 연산자를 이용해, json 파일에 잘못 접근한 경우를 대처하는 코드이다.
catch문에서 잡아줄 에러를 throw를 통해 우리가 직접 보내준다.
let json = '{ "age": 30 }'; // 불완전한 데이터
try {
let user = JSON.parse(json); // <-- 에러 없음
if (!user.name) {
throw new SyntaxError("불완전한 데이터: 이름 없음"); // 에러의 이름과 내용을 담는다
}
alert( user.name );
} catch(e) {
alert( "JSON Error: " + e.message ); // JSON Error: 불완전한 데이터: 이름 없음
}
1-3. try-catch를 더 디테일하게 사용하는 법
모든 에러가 아닌 우리가 예측 가능한 에러를 catch로 다룰 수 있다면 어떨까?
catch문 안에서 특정 에러에 대한 조건을 걸어주어 해당 에러를 throw할 수 있다.
이렇게 된다면 그 다음 try-catch 구문에서 해당 에러를 받아서 다시 처리해줄 수 있다.
function readData() {
let json = '{ "age": 30 }';
try {
// ...
blabla(); // 에러!
} catch (e) {
// ...
if (!(e instanceof SyntaxError)) {
throw e; // 알 수 없는 에러 다시 던지기
}
}
}
try {
readData();
} catch (e) {
alert( "External catch got: " + e ); // 에러를 잡음
}
또한 try-catch-finally 구문을 이용한다면,
해당 에러처리 구문의 예상치 못한 return 처리를 대처할 수 있으며, 항상 실행하는 구문을 보장할 수 있다.
2-1. 동기식언어인 javascript에 비동기 개념이 등장하다 (11.1)
동기적으로 실행될 땐,
작업을 요청했을 때 그 작업이 종료되고 나서야 다음 작업을 수행할 수 있다.따라서 A함수가 B함수를 호출 할 때, B함수의 결과를 A함수가 처리하게 된다.
반대로 비동기적인 상황에서는 A함수가 B함수를 호출 할 때, B함수의 결과를 B함수가 처리하게 된다.기본적으로 javascript는 싱글 스레드 언어로 동기적으로 처리하지만, web api나 event loop를 처리할 때엔 내장 기능을 통해 비동기 처리를 할 수 있게 되었다.
이 때 비동기 처리를 하는 방법 중에 하나가 콜백인 것이다!
2-2. 비동기처리의 해결책, 콜백함수 이해하기
만약 우리가 setTimeout이나, 특정 script 모듈을 추가하는 비동기함수를 처리한다고 하자.
아래 loadScript(src) 함수는 src에 있는 스크립트를 읽어오는 함수이다.
function loadScript(src) {
// <script> 태그를 만들고 페이지에 태그를 추가합니다.
// 태그가 페이지에 추가되면 src에 있는 스크립트를 로딩하고 실행합니다.
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
loadScript('/my/script.js'); // script.js엔 "function newFunction() {…}"이 있습니다.
newFunction(); // 함수가 존재하지 않는다는 에러가 발생합니다!
loadScript 함수는 비동기함수이기 때문에,
loadScript 함수를 실행하고 나서 스크립트 로딩이 끝날 때까지 기다려주지 않는다.
따라서 외부에서 접근하면 비동기함수에 정의된 함수 newFunction()에 접근할 수 없다.
이러한 함수들은 특정 함수를 통해 비동기 동작을 스케줄링하는 과정을 통해 문제를 해결할 수 있다.
그것 중에 하나가 callback함수를 이용하는 것이다.
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
alert(`${script.src}가 로드되었습니다.`);
alert( _ ); // 스크립트에 정의된 함수
});
loadScript('/my/script.js', function() {
// 콜백 함수는 스크립트 로드가 끝나면 실행됩니다.
newFunction(); // 이제 함수 호출이 제대로 동작합니다.
...
});
위처럼 loadScript함수에 callback 인자를 통해, loadScript함수 내에 정의된 로직에 접근할 수 있게 할 수 있다.
외부에서도 해당 로직에 접근할 수 있도록 비동기 동작을 특수하게 처리해주는 것이다.
하지만 만약 스크립트를 2개 처리해줘야하는 경우엔 어떻게 할까?
혹은 그 외 더 많은 스크립트를 처리해야한다면?
2-3. 콜백 지옥이 나타나다
여러 개의 스크립트를 순차적으로 처리하는 방법은, 콜백 함수 안에서 콜백 함수를 호출하는 것이다.
중첩 콜백에서는 바깥의 함수가 완료된 후, 안쪽의 함수가 실행된다.
또한 콜백 중간에 에러를 핸들링을 만들어 줄 수 있다.
아래와 같이 말이다.
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// 모든 스크립트가 로딩된 후, 실행 흐름이 이어집니다. (*)
}
});
}
})
}
});
하지만 보다시피, 위와 같은 코드는 유지보수 측면에서도,
또 하나의 함수가 너무 방대해진다는 측면에서도, 좋지 않은 코드이다.
콜백지옥의 해결책 중 하나로 Promise가 등장하게 된다.
3-1. 콜백지옥의 해결책, Promise
new Promise 객체 안에는 executor 함수를 받게 되어있다.
executor 함수는 resolve와 reject를 인자로 받으며, promise 객체의 내부 프로퍼티를 변경한다.
또한 promise 객체는 아래와 같은 내부 프로퍼티를 가진다.
- state : pending(보류) -> fulfilled(resolve 호출 시) -> rejected(reject 호출 시)
- result : undefined -> value(resolve 호출 시) -> error(reject 호출 시)
참고로 promise 객체는 resolve나 reject 중에 하나엔 꼭 속해야한다.
3-2. Promise 객체를 사용해서 에러를 처리하기
위의 2-2에서 소개한 loadScript함수를 콜백함수가 아닌, Promise를 이용해 처리할 수 있다. 아래와 같다.
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`${src}를 불러오는 도중에 에러가 발생함`));
document.head.append(script);
});
}
loadscript 함수 안에 새로운 promise 객체를 만들어, resolve 혹은 reject 인자를 설정해두었다.
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src}을 불러왔습니다!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('또다른 핸들러...'));
위와 같이 promise의 결과를 then으로 받아와, resolve 혹은 reject의 경우를 처리해주면 된다 !
아무 생각 없이 처리하던 비동기 과정과 통신 개념을 되짚어 볼 수 있던 소중한 시간이었다.
promise 객체로부터 파생되는 정말 다양한 개념이 있다는 것을 알았다.
체이닝을 할 수도, API와 연관지을 수도, promise화를 직접 해줄 수도 있다.
다음에는 promise와 연관된 다양한 개념과, 더 발전된 통신 방법인 async/await를 다뤄보도록 하겠다 :)
'Web > javascript' 카테고리의 다른 글
[Javascript] JS의 함수를 잘 활용하고 있나요? - JS 함수에 숨겨진 기능 톺아보기 (0) | 2023.05.18 |
---|---|
[javascript] 배열 총정리 (0) | 2023.05.06 |
[JS기본] 객체의 모든 것 / 프로퍼티, 생성자함수, this와 메소드, 옵셔널 체이닝 (1) | 2023.04.23 |
[JS 기본] 함수선언문 VS 함수표현식 / 콜백함수, 화살표함수 친해지기 (0) | 2023.04.10 |
[JS 기본] IF문과 논리연산자에 숨겨진 기능 (0) | 2023.04.10 |