[JS] JavaScript 와 Node.js 알쏭달쏭한 개념들
- -
1. 함수는 일급객체
JavaScript에서 함수는 객체이고 거기다가 '일급' 객체 입니다. 때문에 javascript에서는 함수를 arguments로도, 리턴 값으로도 사용할 수 있고 변수에 함수를 넣을 수도 있습니다.
2. Call Back 함수란
CallBack은 말 그대로 나중에 실행되는 코드를 의미합니다. A(a, b, callback) 이라는 함수가 있을 때 A함수의 모든 명령의 실행을 마친 후 마지막으로 넘겨받은 인자 callback을 실행하는 매커니즘이 callback이고 여기서 인자로 들어가는 함수를 '콜백 함수'라고 합니다.
setTimeout(() => { // 내장 함수 setTimeout(callback, delayTime)
console.log('todo: First work!');
}, 3000);
setTimeout(() => {
console.log('todo: Second work!');
}, 2000);
// 결과
todo: Second work!
todo: First work!
JavaScript는 이벤트 중심 언어인데, 어떤 이벤트가 발생하고 그에 대한 결과가 올때까지 기다리지 않고 다음 이벤트를 계속 실행해 버립니다. 따라서 A함수 다음에 B함수를 실행하고 싶은데 A함수가 3초가 걸리고 B함수가 2초가 걸린다면 B함수의 결과가 먼저 반환됩니다.
setTimeout(() => {
setTimeout(() => {
console.log('todo: Second work!');
}, 2000);
console.log('todo: First work!');
}, 3000);
// 결과
todo: First work!
todo: Second work!
처음 원하던대로 A함수의 결과가 반환된 뒤에 B함수를 실행하고 싶으면 '콜백 함수'를 이용해야 합니다. 순차적으로 처리되지 못한 작업을 '비동기' 작업이라고 하고 순차적으로 처리된 작업을 '동기적' 작업이라고 하는데, 비동기 작업을 동기적으로 처리하기 위해 '콜백 함수'는 존재합니다.
3. 콜백의 동기와 비동기
자바스크립트 !내부!의 작업은 동기적으로 처리되고 자바스크립트 !외부!에서 처리되어야 하는 작업은 비동기적으로 처리됩니다. 위 예제에서 setTimeout()함수는 Web API 함수로 자바스크립트 내부에서 처리가 되는 작업이 아닙니다. 이런 것 중 대표적인것 하나가 Ajax 통신으로 어떤 결과를 받아오는 작업이 있습니다. Java, C, Python 등 다른 언어를 사용하다가 JavaScript를 배울 때 이 부분이 개인적으로 익숙하지가 않아서 애를 먹었습니다,, JavaScript는 웹 브라우저 엔진으로 돌아가는 언어입니다. 다른 언어들을 세팅할 때 언어의 실행기가 내 컴퓨터에 설치되는 것과 다르게 웹 브라우저를 통해서야만 실행할 수 있고 그렇지 않으려면 Node.js와 같은 자바스크립트 실행기를 사용해야 하는 이유도 여기에 있습니다.
const syncFunc = () => {
for (let i = 0; i < 100000; i++);
console.log('동기적인 작업');
};
const asyncFunc = () => {
setTimeout(() => {
console.log('비동기적인 작업');
}, 0);
};
asyncFunc(); // 비동기적인 작업
syncFunc(); // 동기적인 작업
setTimeout()의 딜레이 시간을 0초로 설정했고, 이를 먼저 호출했음에도 선언한 순서대로 코드가 실행되지 않습니다. 눈으로 슉 보면 '비동기적인 작업' -> '동기적인 작업' 이렇게 로그가 찍힐 것 같은데.
이해가 잘 되지 않는다면 내가 의도한대로 호출되지 않았다 ---> 비동기 라고 생각하면 됩니다.
4. 블로킹, 논블로킹
Blocking I/O는 호출된 함수가 자신의 작업이 모두 끝날 때까지 다른 작업들은 대기하게 되는 방식입니다.
Non-Blocking I/O는 호출된 함수가 바로 리턴하여 다른 작업을 실행할 수 있도록 하는 방식입니다.
Node.js에서 블로킹 메서드는 동기로 실행되고, 논블로킹 메서드는 비동기로 실행됩니다.
// 파일 읽기 작업 -> 동기로, 블로킹
const fs = require('fs');
const data = fs.readFileSync('/file.md');
// 파일 읽기 -> 비동기, 논블로킹
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
});
블로킹 메서드는 JavaScript 외부의 작업이 완료될때까지 기다리게 되므로 CPU 집약적인 작업을 하면 나쁜 성능을 보여줍니다. 그래서 Node.js 표준 라이브러리의 모든 I/O 는 논블로킹 비동기 방식을 제공합니다. 때문에 억지로 블로킹 메서드를 사용하려면 끝에 Sync가 붙은 이름의 함수를 사용할 수도 있습니다.(서버를 초기화 하는 경우나 딱 한번만 사용할 경우 ex. https를 위한 인증 파일 읽을 때 등)
언뜻 싱글쓰레드면 블로킹 처리를 하지 않을까 생각할 수 있지만 동시에 많은 요청들을 비동기로 수행함으로써 싱글쓰레드라도 논블로킹이 가능하게 됩니다. Node.js에서 블로킹IO를 사용할 때는 별도의 libuv 스레드풀에서 처리하므로 이때는 싱글쓰레드로 처리되지 않지만, 자바스크립트의 메인 쓰레드는 1개입니다.
5. 싱글 쓰레드, 이벤트 루프
function delay() {
for (var i = 0; i < 100000; i++);
}
function foo() {
delay();
bar();
console.log('foo!');
}
function bar() {
delay();
console.log('bar!');
}
function baz() {
console.log('baz!');
}
setTimeout(baz, 0);
foo();
위 코드는 delay()함수가 아무리~~~ 오래 걸리는 작업이라고 해도 결과는 항상 이와 같습니다.
bar!
foo!
baz!
setTimeout(baz, 0)을 제외한 나머지 작업들은 동기적 작업이므로 콜스택에 들어가게 되고, setTimeout은 0초 뒤에 실행되는 것이 아니라 0초 뒤에 작업큐(콜백큐)에 들어가게 됩니다. 그리고 콜스택의 작업들이 모두 비워지지 않는 이상 큐의 작업을 콜스택으로 불러오지 않고 대기하게 됩니다. 따라서 위와 같은 결과가 나오는 것입니다.
javascript에서 "콜스택은 1개" 입니다. 동기적으로 동작하는 작업들은 차례로 call stack에 들어가서 순차적으로 처리가 됩니다. 중간에 비동기적인 작업이 콜백큐(Callback Queue = Job Queue)에 작업이 들어왔다고 해서 멀티 스레드처럼 멈춰서 그 일을 실행하지 않습니다. 콜스택 안의 작업을 끝까지!(심지어 메인함수까지) 처리한 후 큐 작업을 콜스택으로 불러와 처리합니다. 콜스택이 비었는지는 이벤트 루프가 계속 확인합니다.
6. 콜백 지옥 이란
API요청 등 자바스크립트 내부 함수를 사용하지 않는 비동기 함수를 원하는 대로 실행하려면 뜻대로 되지 않기가 십상입니다. 우리가 눈으로 쓱 보는 함수의 동작이 비동기적으로 동작하는 것에 차이가 있기 때문이고, 따라서 내가 원하는대로 실행하려고 하려면 비동기처리를 따로 해주어야 합니다.(내가 원하는 순서대로 함수를 실행할 수 있도록 해주는 것)
하지만 비동기 처리를 위해 위 예시처럼 중첩시켜 코드를 작성하면 '콜백 지옥'이라는 가독성이 어렵고 에러와 예외처리가 어려운 문제가 발생합니다.
setTimeout()처럼 자바스크립트 외부에 있는 함수는 비동기적으로 처리하게 되는데 콜스택이 아닌 콜백큐에 저장되어 있다가 콜스택이 비어지면 실행하기 때문에 에러를 캐치하지 못합니다. 이를 해결하기 위해서는 ES6부터 Promise객체를 제공하고 있고 최근 문법으로는 Async/await을 사용하면 이와 같은 콜백 지옥 문제를 해결할 수 있습니다.
7. 스코프(Scope)
스코프(scope)는 '범위'라는 뜻을 가지고 있고, 프로그래밍에서 스코프라고 하면, '변수에 접근할 수 있는 범위'를 말합니다.
{
var a = 'A';
const b = 'B';
console.log('a : ', a);
console.log('b : ', b);
}
console.log('a : ', a);
console.log('b : ', b);
a : A
b : B
a : A
ReferenceError: b is not defined
과거에 사용하던 var는 함수 스코프를 가지기 때문에 선언되지 않았어도 에러가 나지 않습니다. 하지만 ES6이후부터 사용되는 const, let은 블록 스코프를 가지기 때문에 해당 블록 안에서 참조가 됩니다. 만약 해당 블록에 없다면 다음 블록으로 없다면 전역에서 찾게되는 것입니다.
let은 변수에 재할당이 가능
const는 변수 재선언, 재할당 모두 불가능
8. 변수 호이스팅(Hoisting)
console.log(hello);
var hello = 'hello!'; // undefined
호이스팅(Hoisting)의 뜻은 '끌어 올리다' 입니다. 아직 선언하지 않는 변수를 호출하면 에러가 뜨는 것이 아니라, 값은 나오지 않지만 사용은 할 수 있는 개념입니다.
const hello = 'hello';
function f1() {
console.log(hello);
}
function f2() {
console.log(hello);
const hello = 'hi';
}
f1();
f2();
hello
ReferenceError: Cannot access 'hello' before initialization
const, let도 var처럼 미리 변수 사전에 들어가나(호이스팅이 되지 않는 것이 아님), 기능이 하나 추가됬습니다. 코드를 선언하는 구문에 도달하기 전에는 변수를 사용할 수 없도록 하였습니다.
9. 클로저(Closure)란
function outer() {
var a = 'A';
var b = 'B';
function inner() {
var a = 'AA';
console.log(b);
}
return inner;
}
var outerFunc = outer();
outerFunc(); // B
클로저(closure)는 내부 함수가 외부 함수의 스코프에 접근 할 수 있는 것을 말합니다. 자바스크립트에서 스코프는 함수 단위로 생성되는데 inner()의 스코프가 outer()의 스코프를 참조 하고 있고 outer()의 실행이 끝나고 소멸된 이후에도 inner()가 outer()에 접근할 수 있습니다.
var btns = [
document.getElementById('btn0'),
document.getElementById('btn1'),
document.getElementById('btn2')
];
function setClick() {
for (var i = 0; i < 3; i++) {
btns[i].onclick = function() {
console.log(i);
}
}
}
setClick();
클로저 실수 예시 중 하나,
버튼0을 누르면 0이 나오게 버튼1을 누르면 1이 나오게 하고 싶은데 실제 결과는 어느 버튼을 눌러도 3이 찍힙니다. var는 함수 스코프이기 때문에 setClick() 함수의 스코프에 i 값이 저장되고 for문이 끝난 뒤에 i = 3의 값을 갖고 있게 됩니다. onclick() 함수들이 실행될 때마다 i 값이 내부에 없으므로 다음 외부 함수인 setClick()의 스코프를 참조하기 때문에 이러한 결과가 나오는 것입니다.
var btns = [
document.getElementById('btn0'),
document.getElementById('btn1'),
document.getElementById('btn2')
];
function setClick() {
for (let i = 0; i < 3; i++) {
btns[i].onclick = function() {
console.log(i);
}
}
}
setClick();
따라서 for문의 i를 let으로 선언해주면 되는데, let은 블록 스코프기 때문에 각각의 onclick() 함수 내부의 {}블록마다 스코프가 생성되고 i 값이 저장됩니다. 그치만 여기서도 의문이 i 값이 어떻게 증가하냐 인데,, 이건 for문 내부의 i가 조금 특이하게 동작하기 때문입니다. 이 영상을 참고해주세요. (참고)
10. JavaScript에서 This
자바스크립트에서 'this'는 다른 언어들과 다르게 동작하기 때문에 헷갈리는 개념인데, 자바스크립트에서 this는 호출하는 방법에 의해 결정됩니다. 브라우저 콘솔에서 console.log(this); 를 해보면 Window객체가 나오는데 호출한 애는 글로벌이고 window객체 이므로 이와 같은 결과가 나옵니다.
var people = {
name: 'gildong',
say: function () {
console.log(this);
}
}
people.say();
var sayPeople = people.say;
sayPeople();
{ name: 'gildong', say: [Function: say] }
Object [global] {
global: [Circular],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[Symbol(nodejs.util.promisify.custom)]: [Function]
},
queueMicrotask: [Function: queueMicrotask],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Function]
}
}
people이 say()를 호출했으므로 첫번째 결과는 people 객체가 나오고,
두번째는 전역 변수 즉 글로벌이 호출한애가 되서 global 객체가 반환 됩니다.
var sayPeople = people.say.bind(people);
sayPeople();
{ name: 'gildong', say: [Function: say] }
이런걸 막고 싶다면 bind(this로고정시킬객체) 함수를 사용하면 됩니다.
11. 프로토타입(Prototype)
Prototype의 뜻은 '원형'인데, 자바스크립트로 객체 지향 프로그래밍을 할 수 있게 도와주는 것입니다. 자바스크립트에는 클래스가 없고 '프로토타입'을 통해 비스무리하게 흉내냅니다. 때문에 자바스크립트는 객체 지향 언어라고 하기보단 프로토타입 기반 언어라고 하는 것입니다.
function func() { };
console.log(func.prototype); //func {}
func.prototype.name = 'gildong';
console.log(func.prototype); // func { name: 'gildong' }
자바스크립트에서 기본 데이터타입을 제외한 모든 것은 객체인데, 이 객체의 '원형'인 프로토타입을 이용해서 새로운 객체를 만들어내고 이렇게 생성된 객체는 또 다른 객체의 원형이 되어 새로운 객체를 만들어낼 수 있습니다. 객체는 '프로퍼티'를 가질 수 있는데, prototype은 객체의 프로퍼티 중 용도가 약속되어 있는 특수한 프로퍼티이고 이 역시도 객체입니다.
__proto__객체를 살펴보면 안에 여러가지 프로퍼티들이 기본으로 존재하는 것을 알 수 있습니다. func.hasOwnProperty()라는 프로퍼티도 내가 선언하지 않았지만 프로토타입 객체(__proto__)에 기본으로 저장되어 있어 사용할 수 있게 되는 것입니다.
객체 안에 __proto__라는 프로퍼티가 있고 이 프로퍼티를 만들어낸 원형인 '프로토타입 객체'를 참조하는 숨겨진 링크가 있는데 이 링크를 프로토타입이라고 정의합니다.
const animal = {
leg: 4,
tail: 1,
say() {
console.log('I have 4 legs 1 tail');
}
}
const dog = {
sound: 'wang'
}
const cat = {
sound: 'yaong'
}
dog.__proto__ = animal;
cat.__proto__ = animal;
console.log(dog.leg); // 4
프로토타입이 중요한 이유는 '상속'을 가능하게 한다는 점입니다.
const animal = {
leg: 4,
tail: 1,
say() {
console.log('I have 4 legs 1 tail');
}
}
const dog = {
sound: 'wang',
happy: true
}
dog.__proto__ = animal;
const cat = {
sound: 'yaong'
}
cat.__proto__ = dog;
console.log(cat.happy); // true
console.log(cat.leg); // 4
이렇게 Prototype Chaining도 가능합니다. cat에 happy 프로퍼티가 없으므로 프로토타입인 dog의 프로퍼티를 뒤지고, cat에 leg 프로퍼티가 없으므로 프로토타입인 dog에도 없으니 dog의 프로퍼티인 animal을 뒤집니다.
References
33 Concepts Every JavaScript Develpoer Should Know
understanding Event Loop, Call Stack, Event & Job Queue in JavaScript
'SW 개발' 카테고리의 다른 글
[Node.js로 서버 만들기] 책을 출간하였습니다. (0) | 2021.12.19 |
---|---|
[JS] 자바스크립트 예외처리 (0) | 2021.02.01 |
[NodeJS] 자바스크립트 비동기 연산을 다루는 async/await (0) | 2020.10.20 |
[알고리즘] 완전탐색 문제풀이 _ 파이썬 (0) | 2020.10.06 |
[알고리즘] BFS/DFS 문제 풀이 _ 파이썬 (0) | 2020.09.10 |
소중한 공감 감사합니다