0%

콜백

비동기적 처리에 대해서

1
2
3
4
5
6
7
function loadScript(src) {
// <script> 태그를 만들고 페이지에 태그를 추가합니다.
// 태그가 페이지에 추가되면 src에 있는 스크립트를 로딩하고 실행합니다.
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
1
2
// 해당 경로에 위치한 스크립트를 불러오고 실행함
loadScript('/my/script.js');

loadScript 는 비동기적으로 실행된다.

loadScript()아래에 있는 코드들은 loadScript 가 실행되는 것을 기달려 주지 않고 바로 실행 됨.

1
2
3
loadScript('/my/script.js'); // script.js엔 "function newFunction() {…}"이 있습니다.

newFunction(); // 함수가 존재하지 않는다는 에러가 발생합니다!

newFunction은 loadScript 안에 있으니가 아직 생성 되지 않았을 것이다.

현재로서는 loadScript가 완료 됬는지 알 수 있는 방법이 없다.

loadScript가 완료되면 원하는 함수를 실행 시키기 위해서 콜백 함수를 추가 행야 한다.

1
2
3
4
5
6
7
8
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(script);

document.head.append(script);
}
1
2
3
4
5
loadScript('/my/script.js', function() {
// 콜백 함수는 스크립트 로드가 끝나면 실행됩니다.
newFunction(); // 이제 함수 호출이 제대로 동작합니다.
...
});

실제 사용 예시

1
2
3
4
5
6
7
8
9
10
11
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를 호출하면 된다.

1
2
3
4
5
6
7
8
9
loadScript('/my/script.js', function(script) {

alert(`${script.src}을 로딩했습니다. 이젠, 다음 스크립트를 로딩합시다.`);

loadScript('/my/script2.js', function(script) {
alert(`두 번째 스크립트를 성공적으로 로딩했습니다.`);
});

});

만약 3개라면?

1
2
3
4
5
6
7
8
9
10
11
loadScript('/my/script.js', function(script) {

loadScript('/my/script2.js', function(script) {

loadScript('/my/script3.js', function(script) {
// 세 스크립트 로딩이 끝난 후 실행됨
});

})

});

만약 갯수가 굉장히 많아진다면??

이렇게 작성히는 것은 적을때는 가능하지만 많을때는 좋은 방법이 아니다..


에러 핸들링

스크립트 로딩이 실패했을때 에러 핸들링 추가

1
2
3
4
5
6
7
8
9
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`${src}를 불러오는 도중에 에러가 발생했습니다.`));

document.head.append(script);
}

사용

1
2
3
4
5
6
7
loadScript('/my/script.js', function(error, script) {
if (error) {
// 에러 처리
} else {
// 스크립트 로딩이 성공적으로 끝남
}
});

이런 패턴을 오류 우선 콜백(error-first callback) 이라고 한다.

첫 번째 인수는 에러를 위해 남겨 둔다. 두번재 부터 여러개 추가 할 수 있음. 이렇게 하면 에러 케이스와 성공 케이스 모두 처리 가능하게 된다.


멸망의 피라미드

언뜻 좋아 보이지만 여러개가 반복되면 콜백 지옥이 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 {
// 모든 스크립트가 로딩된 후, 실행 흐름이 이어집니다. (*)
}
});

}
})
}
});

각 동작을 독립적인 함수로 만들어 완화하도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
loadScript('1.js', step1);

function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', step2);
}
}

function step2(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', step3);
}
}

function step3(error, script) {
if (error) {
handleError(error);
} else {
// 모든 스크립트가 로딩되면 다른 동작을 수행합니다. (*)
}
};

사실 이렇게 작성해도 보기 불편한건 사실이고, 함수를 만들었는데 단지 콜백 지옥을 피하기 위해서만 만들었기 때문에 재사용이 불가능하다.

가장 좋은 방법은 프로 미스를 사용하는 방법인데 다음 포스팅에서 설명하도록 한다.

커스텀 이벤트 디스패치

Event의 생성자

내장 이벤트 클래스 계층의 꼭대기엔 Event 클래스가 있다.

1
let event = new Event(type[, options]);
  • type: “click” 같은 내장 이벤트, “my-event” 같은 커스텀 이벤트가 올 수 있음.
  • options
    • bubbles: true/false -> true 일 경우 버블링
    • cancelable: true/false -> true 인 경우 브라우저 ‘기본동작’ 이 실행되지 않는다.
    • 아무런 값도 지정하지 않으면 둘다 false 가 됨.

dispatchEvent

이베트 객체를 생성한 다음에는 elem.dispatchEvent(event)를 호출해 요소에 있는 이벤트를 반드시 실행시켜줘야 한다.

1
2
3
4
5
6
<button id="elem" onclick="alert('클릭!');">자동으로 클릭 되는 버튼</button>

<script>
let event = new Event("click");
elem.dispatchEvent(event);
</script>

event.isTrusted 가 true 이면 사용자 액션을 통해서 만든 이벤트라는 것을 의미. isTrusted 가 false 이면 스크립트를 통해 생성된 이벤트라는 것을 의미


커스텀 이벤트 버블링

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<h1 id="elem">Hello from the script!</h1>

<script>
// 버블링이 일어나면서 document에서 이벤트가 처리됨
document.addEventListener("hello", function(event) { // (1)
alert("Hello from " + event.target.tagName); // Hello from H1
});

// 이벤트(hello)를 만들고 elem에서 이벤트 디스패치
let event = new Event("hello", {bubbles: true}); // (2)
elem.dispatchEvent(event);

// document에 할당된 핸들러가 동작하고 메시지가 얼럿창에 출력됩니다.

</script>
  1. on<event> 는 내장 이벤트에서만 사용 가능 addEventListener를 사용해야 함.
  2. bubbles: true를 명시적으로 써야 버블링 사용 가능

MouseEvent, KeyboardEvent 등의 다양한 이벤트

마우스나 키보드와 관련된 이벤트들은 Event 로 생성하는 것이 아니라 관련 생성자로 생성해야 한다. 그래야 해당 이벤트의 전용 프로퍼티를 명시할 수 있다.

1
2
3
4
5
6
7
8
let event = new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: 100,
clientY: 100
});

alert(event.clientX); // 100

그냥 Event 로 생성하면 무시된다.

1
2
3
4
5
6
7
8
let event = new Event("click", {
bubbles: true, // Event 생성자에선
cancelable: true, // bubbles와 cancelable 프로퍼티만 동작합니다.
clientX: 100,
clientY: 100
});

alert(event.clientX); // undefined, 알 수 없는 프로퍼티이기 때문에 무시됩니다.

커스텀 이벤트

제대로된 커스텀 이벤트를 만들려면 그냥 Event로 생성하는 것이 아니라 CustomEvent로 생성해야 한다.

CustomEvent 로 생성하면 두번째 인수로 detail 이라는 프로퍼티를 추가해 커스텀 이벤트에 대한 정보를 명시할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
<h1 id="elem">이보라님, 환영합니다!</h1>

<script>
// 추가 정보는 이벤트와 함께 핸들러에 전달됩니다.
elem.addEventListener("hello", function(event) {
alert(event.detail.name);
});

elem.dispatchEvent(new CustomEvent("hello", {
detail: { name: "보라" }
}));
</script>

사실 Event로 생성해서 추가 프로퍼티를 넘겨주면 되긴 하지만, 이렇게 생성하면 충돌을 피할 수 있고 무엇보다 직접 만든 커스텀 이벤트라는 명시가 된다.


event.preventDefault()

커스텀 이벤트에는 기본 동작이 없지만 디스패칭 해주는 코드에 원하는 동작을 넣으면, 커스텀 이벤트에도 기본 동작을 설정해줄 수 있다.

event.preventDefault()를 호출하면 elem.dispatchEvent(event) 호출 시 false를 반환한다. 이를 통해서 해당 이벤트에서 기본동작이 취소 되었음을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<pre id="rabbit">
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
</pre>
<button onclick="hide()">hide()를 호출해 토끼 숨기기</button>

<script>
// hide() will be called automatically in 2 seconds
function hide() {
let event = new CustomEvent("hide", {
cancelable: true // cancelable를 true로 설정하지 않으면 preventDefault가 동작하지 않습니다.
});
if (!rabbit.dispatchEvent(event)) {
alert('기본 동작이 핸들러에 의해 취소되었습니다.');
} else {
rabbit.hidden = true;
}
}

rabbit.addEventListener('hide', function(event) {
if (confirm("preventDefault를 호출하시겠습니까?")) {
event.preventDefault();
}
});
</script>

이벤트 안 이벤트

이벤트는 큐로 처리 된다. 따라서 이벤트가 발생되고 다른 이벤트가 또 발생 되면 먼저 발생한 이벤트가 종료 된 이후에 새롭게 발생한 이벤트가 처리 된다.

하지만, 이벤트 안에 또 다른 이벤트 실행되는 경우는 다르다. 스텍 처럼 안쪽에 있는 이벤트 가 먼저 실행 된 이후에 바깥에 있는 이벤트가 실행되게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<button id="menu">메뉴(클릭해주세요)</button>

<script>
menu.onclick = function() {
alert(1);

menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
}));

alert(2);
};

// 1과 2 사이에 트리거됩니다
document.addEventListener('menu-open', () => alert('중첩 이벤트'));
</script>

1, 중첩이벤트, 2 마치 동기적으로 처리되는 것처럼.

때로는 비동기적으로 보이고 싶다면 setTimeout(()=> {}, 0) 사용.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<button id="menu">Menu (click me)</button>

<script>
menu.onclick = function() {
alert(1);

setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
})));

alert(2);
};

document.addEventListener('menu-open', () => alert('중첩 이벤트'));
</script>

프라미스

프라미스를 비유로 설명하면 다음과 같다.

  1. 제작 코드는 시간이 걸리는 일로서 가수가 제작한 코드라고 할 수 있다.
  2. 소비 코드는 제작 코드의 결과를 기다리는 코드로 팬이라고 할 수 있다.
  3. 프라미스는 제작코드와 소비코드를 연결해 주는 특별한 자바스크립트 객체이다. 제작 코드가 완료되면 알려주는 역활을 하며 구독 서비스라고 할 수 있다.
1
2
3
let promise = new Promise(function(resolve, reject) {
// executor (제작 코드, '가수')
});

executor는 promise 가 생성될때 실행된다. resolve와 reject는 js에서 기본으로 제공해 주는 함수로서 개발자는 excutor 만 신경쓰면 된다. 다만, ecutor에서 resolve든 reject든 하나를 반드시 호출해야 한다.

  • resolve(value) - 일이 성공적으로 끝난 경우
  • reject(error) - 에러 발생 시

new Promise 생성자가 반환하는 promise 객체는 다음과 같은 내부 프로퍼티를 갖습니다.

  • state - 처음에 pending 이었다가 resolve 가 호출되면 fulfilled, reject가 호출되면 rejected
  • result - 처음에 undefined 이었다가 resolve 가 호출되면 value, reject 가 호출되면 error

간단한 예시

1
2
3
4
5
6
7
let promise = new Promise(function(resolve, reject) {
// 프라미스가 만들어지면 executor 함수는 자동으로 실행됩니다.

// 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result는 'done'이 됩니다.
setTimeout(() => resolve("done"), 1000);
});

프로미스 객체의 변화

반대로 거절한 경우

1
2
3
4
let promise = new Promise(function(resolve, reject) {
// 1초 뒤에 에러와 함께 실행이 종료되었다는 신호를 보냅니다.
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

프로미스 객체의 변화

이행이나 거부된 프로미스를 처리된(settled)된 프라미스라고 한다. 반대로 처리가 안된 프라미스는 pending 상태라고 한다.

  • 프라미스는 성공 또는 실패만 합니다. 성공했다가 실패하고 이렇게 할 수 없다.
  • reject는 어떤 객체도 인수로 넘겨 줄 수 있지만 Error 객체를 사용할 것을 추천한다. 혹은 Error 객체를 상속 받는 객체를 사용할것.
  • state와 resulot는 내부 프로퍼티로 개발자가 직접 접근할 수 없다. then catch finally 를 사용해야 접근 가능

소비자: then, catch, finally

프라미스는 excutor와 소비함수를 이어주는 역할을 한다. 소비 함수는 then, catch, finally 를 사용하여 등록한다. (구독)

then

1
2
3
4
promise.then(
function(result) { /* 결과(result)를 다룹니다 */ },
function(error) { /* 에러(error)를 다룹니다 */ }
);

성공적으로 이행된 프라미스가 어떻게 반응하는지

1
2
3
4
5
6
7
8
9
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});

// resolve 함수는 .then의 첫 번째 함수(인수)를 실행합니다.
promise.then(
result => alert(result), // 1초 후 "done!"을 출력
error => alert(error) // 실행되지 않음
);

프라미스가 거부된 경우 두번째 함수가 실행된다.

1
2
3
4
5
6
7
8
9
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

// reject 함수는 .then의 두 번째 함수를 실행합니다.
promise.then(
result => alert(result), // 실행되지 않음
error => alert(error) // 1초 후 "Error: 에러 발생!"를 출력
);

작업이 성공적으로 처리된 경우만 다루고 싶다면 .then 인수로 하나만 전달하면 된다.

catch

에러를 처리하고 싶다면 .then(null, errorHandlerFunction)과 같이 사용하면되는데, .catch(errorHandlerFunction) 을 사용해도 동일하게 작동한다.

1
2
3
4
5
6
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

// .catch(f)는 promise.then(null, f)과 동일하게 작동합니다
promise.catch(alert); // 1초 뒤 "Error: 에러 발생!" 출력

finally

무조건 실행되는 함수 를 등록한다. .then(f, f)와 유사하다.

1
2
3
4
5
6
new Promise((resolve, reject) => {
/* 시간이 걸리는 어떤 일을 수행하고, 그 후 resolve, reject를 호출함 */
})
// 성공·실패 여부와 상관없이 프라미스가 처리되면 실행됨
.finally(() => 로딩 인디케이터 중지)
.then(result => result와 err 보여줌 => error 보여줌)
  1. finally 는 보변적으로 처리되어야 하는 함수를 등록하기 때문에 성공 실패 여부를 모른다. finally 핸들러엔 인수가 없다.
  2. 자동으로 다음 핸들러에 결과와 에러를 전달한다.
1
2
3
4
5
new Promise((resolve, reject) => {
setTimeout(() => resolve("결과"), 2000)
})
.finally(() => alert("프라미스가 준비되었습니다."))
.then(result => alert(result)); // <-- .then에서 result를 다룰 수 있음

finally는 결과를 처리하기 위해서 만들어진게 아니기 때문에 통과해서 다음 으로 전달된다.

예시 : loadScript

1
2
3
4
5
6
7
8
9
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`${src}를 불러오는 도중에 에러가 발생함`));

document.head.append(script);
}
1
2
3
4
5
6
7
8
9
10
11
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);
});
}

사용법

1
2
3
4
5
6
7
8
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('또다른 핸들러...')); // 원하는 만큼 등록 가능.

프라미스 체이닝

프라미스 체이닝을 이용한 비동기 처리에 대해서 알아본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
})
.then(function(result) {
console.log(result); // 1
return result * 2;
})
.then(function(result) {
console.log(result); // 2
return result * 2;
})
.then(function(result) {
console.log(result); // 4
return result * 2;
})

이런게 되는 이유는 .then 을 호출하면 프라미스가 반환되기 때문이다.
한편으로는 핸들러에서 반환하는 값이 프라미스의 result가 된다.

초보자들이 흔히 하는 실수 중 하나는 프라미스 하나에 여러가지 .then 을 추가한 후 에 체이닝이라고 생각하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
})

promise.then(function(result) {
console.log(result) // 1
return result * 2;
})

promise.then(function(result) {
console.log(result) // 1
return result * 2;
})

promise.then(function(result) {
console.log(result) // 1
return result * 2;
})

프라미스 반환하기

.then으로 등록한 핸들러에서 프라미스를 반환하는 경우도 있다.
이 경우에는 프라미스 처리가 완료된 이후에 다음 .then 이 호출된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
})
.then(function(result) {
console.log(result) //1
return new Promise(function(resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
})
})
.then(function(result) {
console.log(result) //2
return new Promise(function(resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
})
})
.then(function(result) {
console.log(result) //4
})

1초에 시간씩 기다린 후에 표시 된다.


loadScript 예시 개선하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// 불러온 스크립트 안에 정의된 함수를 호출해
// 실제로 스크립트들이 정상적으로 로드되었는지 확인합니다.
one();
two();
three();
});

화살표 함수를 이용해서 줄이기

1
2
3
4
5
6
7
8
9
loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
// 스크립트를 정상적으로 불러왔기 때문에 스크립트 내의 함수를 호출할 수 있습니다.
one();
two();
three();
});

thenable : .then 이라는 메서드를 가진 객체는 모두 thenable이라고 부른다. 핸들러는 모두 thenable객체를 반환한다고도 할 수 있다 이점을 활용해서 Promise를 상속받지 앋고도 커스텀 객체를 사용해 프라미스 체이닝을 만들 수 도 있다.


fetch와 체이닝 함께 응용하기

기본 문법

1
let promise = fecth(url);

해당 url로 요청을 보내고 응답을 기다리는 프라미스를 반화한다.
그런데 해당 프라미스는 응답이 완전히 완료 되기전에 이행상태가 되어버린다.
fetch.text() 를 호출해서 테스트가 완전히 다운로드 되면 result 값으로 갖는 이행된 프라미스를 반환하게 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
fetch('/example/user.json')
// 원격 서버가 응답하면 .then 아래 코드가 실행됩니다.
.then(function(response) {
// response.text()는 응답 텍스트 전체가 다운로드되면
// 응답 텍스트를 새로운 이행 프라미스를 만들고, 이를 반환
return response.text()
})
.then(function(text) {
// 원격에서 받아온 파일의 내용
console.log(text);
})

그러나 reponse.json()을 사용하면 json으로 파싱까지 할 수 있다.

1
2
3
4
// 위 코드와 동일한 기능을 하지만, response.json()은 원격 서버에서 불러온 내용을 JSON으로 변경해줍니다.
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => alert(user.name)); // iliakan, got user nam

GitHub 에 요청을 보내 사용자 프로필을 불러오고 아바타를 출력해 보는 예제

1
2
3
4
5
6
7
8
9
10
11
12
fetch('article/promise-chaining/user.json')
.then(response => reponse.json())
.then(user => fetch(`http://api.github.com/users/${user.name}`))
.then(reponse => reponse.json())
.then(githubUser => {
let img = documnet.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
documnet.body.append(img);

setTimeout(() => img.remove(), 3000); // (x)
})

x 부분에 흔히들 한흔 실수가 있다.
만약 아바타를 잠깐 보였다가 사라진 이후에 무언가를 하고 싶으면 어떻게 해야 할까?
지금으로선 방법이 없는데 체인을 사용해 프로미스를 반환하도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise(function(resolve, reject) { // (*)
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);

setTimeout(() => {
img.remove();
resolve(githubUser); // (**)
}, 3000);
}))
// 3초 후 동작함
.then(githubUser => alert(`Finished showing ${githubUser.name}`));

이제 사진이 사라진 후에 동작을 정의할 수 있게 됬다.

결론 적으로 말하면 비동기 동작을 할때는 항상 프라미스를 반환하는 것이 좋다.
지금은 당장 쓰지 않더라도 나중에 확장하기 용이하기 때문이다.

.then(f1).catch(f2) vs .then(f1, f2) 다를까?

다르다.
.then(f1).catch(f2)의 경우 f1 에서 에러가 발생했다면 catch 에서 처리 할 수 있다.

반면에, .then(f1, f2)의 경우에는 f1에서 에러가 발생하면 이어지는 체인이 없기 때문에 처리할 수 없다.

프라미스와 에러 핸들링

존재하지 않는 url로 fetch 하는 예제를 살펴본다.

1
2
3
fetch("https://no-such-server.blabla")  // 거부
.then(reponse => reponse.json)
.catch(console.log) // TypeError

암시적 try..catch

예외가 발생하면 암시적 보이지 않는 암시적 try..catch 에서 에러를 잡고 이를 reject처럼 다룬다.
따라서 .catch 를 통해서 던져진 에러를 핸들링 할 수 있다.

executor 안에서 잡혀진 에러는 거부된 상태에 파리미스로 반환된다.
따라서 제어 흐름이 가장 가까운 에러 헨들러로 넘어간다.


다시 던지기

마지막에 .catch 를 통해서 여러개의 .then 에서 발생한 모든 에러를 처리할 수 있다.
try..catch에서 처리할수 없는 에러를 다시 던진 것처럼 프라미스에서도 비슷하게 처리할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//실행 순서 : catch -> catch
new Promise((resolve, reject) => {
throw new Error("에러 발생");
})
.catch(err => {
if (err instanceof URIError) {
// 에러처리
} else {
console.log('처리할 수 없는 에러');
throw err; // 에러 다시 던지기
}
})
. then(() => {
// 여기는 실행되지 않는다.
// 에러가 잘 처리되지 않고 다시 던져졌기 때문에
})
.catch(err => {
console.log(`알수 없는 에러 발생: ${err}`);
});

처리되지 못한 거부

에러가 발생했는데 만약 .catch를 통해서 잡지 않았다면 에러가 갇히게 된다.
실무에서는 끔찍한 상황이 벌어지게 된다.

만약 에러를 처리하지 못했다면 전역 에러를 발생시킨다. 부라우저 환경에선 에러가 발생했는데 .catch 가 없으면 unhandledrejection 핸들러가 트리거 된다. 이 이벤트로 원하는 작업을 할 수 도 있다.


executor 안에서 비동기적으로 발생한 에러

1
2
3
4
5
new Promise(function(resolve, reject) {
setTimeout(() => {
throw new Error("에러 발생!");
}, 1000);
}).catch(alert);

위 코드에서 암시적 try..catch로 인해 catch에서 에러가 처리될거 같지만, 실제로는 setTimout에 의해서 비동기적으로 처리되는 부분에서 에러가 발생했으므로 catch에서 에러가 처리되지 않는다.

프라미스 API

Promise 클래스에는 5가지 정적 메서드가 있다.

Promise.all

복수의 Promise를 처리할 때

1
let promise = Promise.all([...promises...])

배열로 넘겨준 promises 가 모두 처리가 끝나면 결과값을 담은 배열을 result 로 하는 새로운 promise를 반환한다.

1
2
3
4
5
Promise.all([
new Promise(resolve => setTimeout(()=> resolve(1), 3000)),
new Promise(resolve => setTimeout(()=> resolve(2), 2000)),
new Promise(resolve => setTimeout(()=> resolve(3), 1000))
]).then(console.log);

프라미스는 3초후에 실행되고 결과값인 result[1,2,3] 이 된다.
처리된 순서가 아니라 배열로 넘겨준 순서이다.

fetch에 응용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://api.github.com/users/jeresig',
]

// fetch를 이용해서 url을 promise로 매핑
let requests = urls.map(url => fetch(url));

Promise.all(requests)
.then(responses => responses.forEach(
response => console.log(response.url + response.status)
));

Promise.all에서 넘겨준 배열에서 하나라도 에러가 발생하면 에러가 발생한 시점에서 모든 프로미스가 거부되고 .catch가 실행된다.

Promise.all에 넘겨주는 값이 이터러블 객체가 아니더라도 값을 넘겨줄수 있다. 값을 넘겨주면 넘겨준 값 그대로 반환 된다.
이미 값을 구한 프라미스는 그냥 전달해 주기만 하면 된다.

1
2
3
4
5
6
7
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2,
3
]).then(console.log); // 1, 2, 3 (1초 후 )

Promise.allSettled

구식 브라우저에서는 폴리필 필요

모든 프라미스가 처리될 때까지 기다린다.

  • 응답이 성공한 경우 - {status: "fulfilled", value: result}
  • 에러가 발생한 경우 - {status: "rejected", reason: error}

fetch 로 보낸 여러 요청 중 실패하더라도 성공한 요청은 남아있어야 하는 경우라면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let urls = [
'https://api.github.com/users/jayoonKoo',
'https://no-such-url',
];

Promise.allSettled(urls.map(url => fetch(url)))
.then(result => {
result.forEach((result, num) => {
if (result.status == "fulfilled") {
console.log(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
console.log(`${urls[num]}: ${result.reason}`);
}
});
});

폴리필

1
2
3
4
5
6
7
8
9
10
11
if(!Promise.allSettled) {
Promise.allSettled = function(promises) {
return Promise.all(promises.map(p=> Promise.resolve(p).then(value => ({
status: 'fulfilled',
value
}), reason => ({
status: 'rejected',
reason
}))));
};
}

Promise.race

가장 먼저 처리되는 프라미스의 결과(혹은 에러)를 반환한다.

1
let promise = Promise.race(iterable);
1
2
3
4
5
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("애러 발생")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
]).then(console.log); // 1

Promise.resolve/reject

async/awiat 문법이 나온 이후로 잘 사용하지 않음.

Promise.resolve

다음과 같음.

1
let promise = new Promise(resolve => resolve(value));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let cache = new Map();

function loadCached(url) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // (*)
}

return fetch(url)
.then(response => response.text())
.then(text => {
cache.set(url,text);
return text;
});
}

Promise.reject

다음과 같음.

1
let promise = new Promise((resolve, reject) => reject(error));

프로미스화

콜백을 받은 함수를 프라미스를 반환하는 함수로 바꾸는 것을 ‘프라미스화(promisification)’ 이라고 한다.

콜백 예시

1
2
3
4
5
6
7
8
9
10
11
12
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`${src}를 불러오는 도중에 에러가 발생함`));

document.head.append(script);
}

// usage:
// loadScript('path/script.js', (err, script) => {...})

이제 이 콜백을 프로미스화 할 것임. 해당 함수는 콜백을 인수로 받지 않고 src만 인수로 받도록 함.

1
2
3
4
5
6
7
8
9
10
11
let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err);
else resolve(script);
})
})
}

// usage:
// loadScriptPromise('path/script.js').then(...)

loadScriptPromiseloadScript에 모든 일을 위임한다.

그런데 실무에서는 여러 함수를 프라미스화 해야 할 것이다. 래퍼 함수를 만들 도록 한다. 프라미스화를 적용할 함수 f를 받고 래퍼 함수를 반환하는 함수 promisefy(f)를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function promisify(f) {
return function (...args) { // 래퍼 함수를 반환함
return new Promise((resolve, reject) => {
function callback(err, result) { // f에 사용할 커스텀 콜백
if (err) {
reject(err);
} else {
resolve(result);
}
}

args.push(callback); // 위에서 만든 커스텀 콜백을 함수 f의 인수 끝에 추가함.

f.call(this, ...args); // 기존 함수를 호출
});
};
};

// 사용법:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);

함수 f가 두 개를 초과하는 인수를 가진 콜백, callback(err, res1, res2, ...) 을 받는 경우?

promisify(f, true) 형태로 호출하면, 프로미스 결과는 콜백의 성공 케이스 (result)를 담은 배열 [res1, res2, ...] 이 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 콜백의 성공 결과를 담은 배열을 얻게 해주는 promisify(f, true)
function promisify(f, manyArgs = false) {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, ...results) { // f에 사용할 커스텀 콜백
if (err) {
reject(err);
} else {
// manyArgs가 구체적으로 명시되었다면, 콜백의 성공 케이스와 함께 이행 상태가 됩니다.
resolve(manyArgs ? results : results[0]);
}
}

args.push(callback);

f.call(this, ...args);
});
};
};

// 사용법:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...)

es6-promisify 같은 프라미스화를 도화주는 모듈도 있다.
또한, node.js에선 내장 함수 util.promisify를 사용해 프라미스화를 할 수 있다.

마이크로태스크

프로미스 핸들러는 항상 비동기적으로 실행된다.
.then().catch().finally() 와 같은 함수 뒤에 적혀 있는 코드가 있다면 뒤에 적혀 있는 코드가 항상 먼저 실행괴고 그 다음 실행된다.

1
2
3
4
5
let promise = Promise.resolve();

promise.then(() => console.log('프라미스 성공'));

console.log('코드 종료'); //이 로그가 가장 먼저 실행된다.

마이크로태스크 큐

비동기 작업을 위한 큐가 있다. ECMA에서는 PromiseJobs라고 부른다. V8 엔진에서는 이를 마이크로테스크 큐 라고 부른다.

  • FIFO 정책을 따른다.
  • 실행될 코드가 전혀 없을 때 마이크로테스크 큐에 작업이 실행된다.

.then().catch().fianlly() 는 항상 비동기적으로 실행 된다. 이 작업들은 모두 마이크로테스크 큐에 담기게 되고, 현제 실행될 코드가 없고 이전에 먼저 큐에 담겨진 코드가 모두 실행된 이후에 해당 작업이 실행된다.

처리되지 못한 거부

처리되지 못한 거부는 마이크로태스크 큐 끝에서 프라미스 에러가 처리되지 못할 때 발생한다.

정상적인 경우 .catch()를 호출해서 에러를 처리하지만 그렇지 못한 경우 엔진은 마이크로태스크 큐가 빈 이후에 unhandlerejection 이벤드를 트리거 한다.

1
2
3
4
let promise = Promise.reject(new Error("프라미스 실패!"));

// 프라미스 실패!
window.addEventListener('unhandledrejection', event => alert(event.reason));

그런데 만약 setTimout()을 사용하여 에러를 나중에 처리하면

1
2
3
4
5
let promise = Promise.reject(new Error("프라미스 실패!"));
setTimout(() => promise.catch(err => console.lot('잡았다')), 1000)


window.addEventListener('unhandlerejection', event => console.log(event.reason));

프라미스 실패가 먼저, 잡았다가 나중에 출력되는 것을 볼 수 있다.

unhandlerejection은 마이크로테스크 큐에 모든 작업이 완료 되면 트리거 된다. 엔진은 프라미스를 검사하고 하나라도 거부 상태이면 핸들러를 트리거 한다.
.catch()역시 트리거 된다. 다만 .catch()unhandledrejection이 발생한 이후에 트리거 된다.

운영체제 큰 그림

운영체제 역할 : 시스테 자원 (System Resouce) 관자자

  • Operating System
  • System Resouce = Computer 하드웨어
    • CPU, Memory
    • I/O Devices
    • 저장 매체

하드웨어는 자체로는 아무것도 할 수 없다.
CPU를 얼마만큼 사용할지, 어디 메모리 주소에 저장할지, 마우스 조작은 어떻게 할지, 어떤 폴더에 파일을 저장할지 등에 모든 결정은 운영체제가 한다.


대표적인 운영체제

  • Windoes OS
  • Mac OS
  • UNIX
  • UNIX 계열 OS
  • LINUX

운영체제의 역할 : 사용자와 컴퓨터 간의 커뮤티케이션 지원

운영체제 안의 쉘이라는 소프트웨어가 하드웨어와 사용자간의 커뮤니케이션을 지원한다.


운영체제 역할 : 컴퓨터 하드웨어와 프로그램을 제어

하드웨어 뿐만 아니라 프로그램 까지도 제어하는 역활을 수행한다.


응용 프로그램이란?

소프트웨어 = 운영체제, 응용프로그램(엑셀, 파워포인 같은..)

응용 프로그램 = Application


운영체제와 응용 프로그램간의 관계

운영체제는 응용 프로그램을 관리한다.

  • 실행 시킴
  • 응용 프램간의 권한을 관리한다.
  • 사용하는 사용자도 관리한다. (로그인…)

컴퓨터 구조

컴퓨터를 키면? 운영체제는 Memory에 올라감.

현대의 컴퓨터는 폰노이만 구조를 따르고 있기 때문에

메모리에 올려놓기 실행.

운영체제 History

1950 ~ 1960

1950 년대

eniac: 첫 번째 컴퓨터

당시에는 응용프로그램 1개를 돌리기도 버거웠다.

운영체제는 없었고 운영체제가 해야할 시스템 자원 관리는 응용프로그램 자신이 했다.

1960년대 초기

응용 프로그램도 늘어나고 사용자도 증가하기 시작했다.

배치 처리 시스템 (batch processing system) 등장

  • 여러 사용자가 생기면서 프로그램이 다른 사용자의 사용이 끝날때까지 기다려야하는 문제 발생
  • 다음에 실행될 파일을 순차적으로 등록할 수 있는 시스템 개발, 이를 기반으로 운영체제가 출현

1960년대 후반

새로운 개념이 제안됨

  1. 시분할 시스템
  2. 멀티태스킹

당시에는 운영체제에서 구현되지는 않았음.

응용프로그램이 CPU를 사용하는 시간을 잘개 쪼개서 여러개의 프로그램을 동시에 실행하는 것처럼 보이게 함.

시분할 시스템

기본 목적은 다중 사용자를 지원하기 위함이였고 이를 위해서 응용프로그램이 CPU를 사용하는 시간을 잘게 쪼게서 컴퓨터 응답시간을 최소화 함.

멀티 태스킹

단일 CPU에서 시간을 잘게 나눠서 프로그램을 실행함으로써 마치 병렬로 응용프로그램이 실행되도록 보이게 하는 기능

시분활 시스템과 멀티태스킹은 비스한 개념으로 많이 사용됨.

멀티프로그래밍? 최대한 CPU를 많이 활용하도록 하는 시스템 (시간 대비 CPU 활용도 높이기)


1970년대

UNIX의 탄생.

제대로 된 운영체제가 처음 개발되었다.

NNIX 특징

현대 운영체제의 기본 기능을 모두 지원하였다.

  • 멀티 태스킹
  • 시분할 시스템
  • 멀티 프로그래밍

다중 사용자를 지원했다.


1980년대

개인용 컴퓨터가 개발되었다.

들어가기전에 용어정리

  • CLI : Comand Line Interface(키보드로만 조작하는 인터페이스) ex) terminal
  • GUI : Graphical User Interface(그래픽 요소가 있는 인터페이스) 마우스로 조작 가능

1980년대 후반으로 가면서 CLI 에서 GUI로 Interface가 변경되기 시작했다.


1990년대

  1. 수많은 응용프로그램이 GUI 환경에 힘입어 개발되었다.
  2. 네트 워크 기술이 발달되었다. - 월드 와이드 맵(www) 대중화
  3. 오픈 소스 운동 활성화 시작 -> UNIX 계열 Os 와 오픈소스 응용 프로그램 개발
    • LINUX 운영체제 개발

2000년대 이후

  1. 오픈 소스 활성화
  2. 가상 머신, 대용량 병렬 처리 등 활성화