컴퓨터 공부/🕸️ Web

Javascript 중급 - 1) this, bind, call, apply, 동기/비동기, call stack, call back

letzgorats 2023. 12. 4. 16:18

< this 키워드 >

- this 키워드는 여러상황에서 각기 다른 것들을 참조한다.

 

1) 메소드에서 this 를 사용하면, 해당 객체를 가리킨다(참조한다)

// // Method => object
const audio = {
    title: 'ALLU',
    play() {
        console.log('play this', this);
    }
}

audio.play();

audio.stop = function () {
    console.log('stop this', this)
}

audio.stop();

 

메소드에서는 this는 해당 객체를 가리킨다.

 

2) 함수에서 this를 사용하면, window 객체를 가리킨다.

// Function => Window Object
function playAudio() {
    console.log(this);
}

playAudio();

function 이라고 함수를 명시해주면 this는 window 객체를 가리킨다.

 

3) constructor 함수(생성자 함수)에서 this를 사용하면, 빈 객체를 가리킨다.

// Constructor Function => {}
function Audio(title) {
    this.title = title;
    console.log(this);
}

const audio = new Audio('ALLU');

 

생성자 함수에선 this가 빈 객체를 가리킨다. 그래서 this.title 이 없으면, 빈 객체를 출력한다.

 

그렇다면, 아래와 같은 코드에서는 this는 어떤 것을 가리킬까?

const audio = {
    title: 'audio',
    categories: ['pop', 'hiphop', 'jazz','rock'],
    displayCategories() {
        this.categories.forEach(function (category) {
            console.log(`title: ${this.title}, category: ${category}`);
        };
    }
}

audio.displayCategories();

 

this.title의 this는 function(category) 함수 안에 있는 this 이다. 반면, this.categories의 this 는 displayCategories() 가 메소드이기 때문에 메소드 안에 있는 this 라고 생각할 수 있다.

위 코드를 실행해보면, 결과는 아래와 같이 나온다. 

title은 undefined

왜 이런 결과가 나왔을까?

내부에 있는 this.title에서 this 가 가리키는 것은 window 객체이다. 왜냐하면 함수 안에 있는 this 이기 때문이다.

winodw 객체는 title 이라는 속성을 가지고 있지 않아서 undefined이 출력되는 것이다.

 

그럼, forEach 안에 있는 함수의 this가 다른 객체를 참조하게 하려면 어떻게 해야 할까? 코드를 아래와 같이 한 번 바꿔보자.

const audio = {
    title: 'audio',
    categories: ['pop', 'hiphop', 'jazz','rock'],
    displayCategories() {
        this.categories.forEach(function (category) {
            console.log(`title: ${this.title}, category: ${category}`);
        },this) ;
    }
}

 

displayCategories 메소드의 두 번째 인자로 this를 줬다. 물론, 아래와 같이 직접적으로 인자를 줘도 된다.

const audio = {
    title: 'audio',
    categories: ['pop', 'hiphop', 'jazz','rock'],
    displayCategories() {
        this.categories.forEach(function (category) {
            console.log(`title: ${this.title}, category: ${category}`);
        },{title:'audio'}) ;
    }
}

audio.displayCategories();

 

공식문서를 찾아보면, forEach의 첫 번째 매개변수는 콜백함수이고, 두번째 매개변수인, thisArg에 넣는 것은 콜백함수에서 this 로 참조할 수 있게 되는 것이라고 한다.

 

이렇게 되면, displayCategories()는 메소드이기 때문에, 해당 객체를 가리키게 되고, 현재 객체는 audio로, 아래와 같이 출력되는 것을 확인할 수 있다.

메소드에서의 this 는 해당 객체 참조

이걸 더 발전시켜보면, 아래와 같이 코드를 짤 수 있다.

const audio = {
    title: 'audio',
    categories: ['rock', 'pop', 'hiphop', 'jazz'],
    displayCategories() {
        this.categories.forEach((category) => {
            console.log(this);
        });
    }
}

audio.displayCategories();

 

위와 같이 화살표 함수에서 this 는 항상 상위스코프의 this를 가리키게 된다. 여기서 console.log(this)에서의 this의 상위스코프는 this.categories의 this이니까, 즉 displayCategories 메소드의 this 니까 해당 this는 audio 라는 객체를 가리키게 되는 것이다.

 

4) 화살표 함수에서의 this는 항상 상위 스코프의 this를 가리킨다. 이것을 Lexical this 라고 부른다.

lexical this


< bind, call, apply >

위와 같이 보통 함수 안에서 this 를 사용하면 window 객체를 참조하게 되는데요, 이걸 바꿀 수 있는 방법은 없을까?

해결책은 3가지로 나눌 수 있다.

 

1)  call() 메소드 사용하기

- call 메소드는 함수를 호출하는 함수이며, 첫 번째 매개변수로 어떠한 것을 전달해주면 호출되는 함수의 this 안에 window 객체가 아닌 전달받은 것을 받게 된다.

// Call();
const fullName = function () {
    console.log(this.firstName + " " + this.lastName);
}

const dog1 = {
    firstName: "Allu",
    lastName: "Kim"
}

// This will return "Allu Kim"
fullName.call(dog1);

 

함수를 호출하는 call 메소드를 사용해서 인자로 dog1 이라는 객체를 줬다. 원래는 함수 안의 this는 window 객체를 가리키지만, call() 함수에 인자를 줌으로써 특정 객체를 가리키게 할 수 있게 된 것이다.

this 가 가리키는 dog1

// Call() with arguments
const fullName = function (city, country) {
    console.log(`${this.firstName}, ${this.lastName}, ${city}, ${country}`);
}

const dog1 = {
    firstName: "Allu",
    lastName: "Kim",
}

fullName.call(dog1, "San fransico", "US");

 

call 메소드 같은 경우, 인수를 넣어서 사용할 수도 있는데, 객체를 인수로 주고, 다른 값들도 인수로 준다면, 해당 값들도 ${parameter}를 통해서 접근할 수 있다.

call 메소드에서의 인수

 

2) apply() 메소드 사용하기

- apply 메소드는 call 메소드와 비슷하지만, 인수 부분을 배열로 넣어줘야 한다.

// Apply() with arguments
const fullName = function (city, country) {
    console.log(`${this.firstName}, ${this.lastName}, ${city}, ${country}`);
}

const dog1 = {
    firstName: "Allu",
    lastName: "Kim",
}

fullName.apply(dog1, ["San fransico", "US"]);

 

apply 메소드에서는 call() 과 비슷하지만, 다른 인수들을 인자 순서에 맞춰 배열 형태로 줘야 합니다.

apply 메소드에서는 배열형태로 인수전달 - call과 똑같은 결과가 나온다.

 

3) bind() 메소드 사용하기

- bind 메소드를 이용해서도 함수에서 this 가 window 객체 대신 다른 게 나오게 할 수 있다. call 과 apply 와는 다르게, bind 는 직접 함수를 실행하지 않고 바인딩만 시켜주는 것이다.(그냥 반환만 한다.)

// bind()
function func(whoseDog) {
    if (whoseDog === "letzgorats") {
        console.log(`DogName: ${this.myDog}`);
    } else {
        console.log(`DogName: ${this.yourDog}`);
    }
}

const whoseDog = {
    myDog: "알루 ",
    yourDog: "초코 ",
};

// boundFunc에다가 바인딩된 func 함수를 할당해서
const boundFunc =  func.bind(whoseDog);
// 호출해야 실행된다.
boundFunc('letzgorats');
boundFunc('user2');

 

위와 같이 bind() 는 call()과 apply() 처럼 인자를 줌과 동시에 호출이 되는 것이 아니라 바인딩만 시켜주는 것이다. 이를 바로 호출까지 하려면 아래와 같이 코드를 짤 수 있다.

const boundFunc =  func.bind(whoseDog)("letzgorats"); // DogName: 알루 가 출력

 

binding 된 함수를 호출해야 실행


< 조건부 삼항 연산자 >

- Conditional Operator 라고 부르며 보통 if문으로 분기를 칠 때 아래와 같이 코드를 짠다.

if (condition) {
    alphabet = 'a';
} else {
    alphabet = 'b';
}

 

하지만, 이런 if 문을 한 줄의 코드로 작성하면 다음과 같이 작성할 수 있다.

condition ? alphabet = "a" : alphabet = "b" ;

 

condition 이 true 이면 콜론(:) 왼쪽이, false 이면 콜론(:) 오른쪽이 실행된다.


< Event Loop >

- 먼저 다음과 같은 코드가 있다고 생각해봅시다.

console.log('1');

setTimeout(() => {
    console.log('2');
}, 3000);  // 	3초 이따가 실행

console.log('3')

 

가장 먼저, console.log('1') 을 통해 1이 출력이 되고, console.log('3') 을 통해 '3'이 출력이 되고, 마지막으로 '2'가 출력된다.

1 -> 3 -> 2

 

이처럼, setTimeout() 메소드는 지정된 함수나 지정한 코드 조각을 실행하는 타이머를 설정한다. (단위는 ms 이다.)

여기서, console.log('1') 과 console.log('3') 은 동기이고 setTimeout() 부분은 비동기라고 할 수 있다.

 

※ 그렇다면, 동기와 비동기가 뭘까? 동기와 비동기의 차이에 대해서 짚고 넘어가보자.

 

동기 - Synchronous 시간을 맞춤

: 예를 들어서, 대학생이 되기 위해서는 고등학교 1학년, 2학년, 3학년을 끝내고고 혹은 검정고시를 통과하고 대학교를 가야하는 시간적 루트가 있다.

 

비동기 - Asynchronous 시간을 맞추지 않음

: 예를 들어서, 취직을 하기 위해서는 개발공부를 하고, 토플 공부를 하고, 자격증을 취득하고, 영어를 공부하 등을 한다고 해보자. 이들 항목 사이에는 특별히 시간의 순서가 정해져 있지 않다. 자격증을 취득하면서 영어공부도 할 수 있고 취업을 한 이후에도 개발 공부를 할 수 있기 때문이다.

 

이 둘의 차이는 쉽게 말해 아래와 같이 말할 수 있다.

"동기는 먼저 이전의 것이 끝나야 다음 것을 할 수 있지만, 비동기는 1번을 하면서 2번을 할 수 있고, 3,4번도 할 수 있는 것"

 

 

JavaScript 는 한 줄 실행하고 또 다음 줄을 실행하는 동기 언어이다.

하지만, 위에 setTimeout 에 콜백 함수를 실행하는 비동기 코드를 사용했다. 이것은 어떻게 된 것일까?

setTimeout(() => {
    console.log('1');
}, 1000);
console.log('2');

 

비동기 코드를 작성하기 위해서 자바스크립트 이외의 도움을 받는다!


위 코드에서 setTimeout도 보면 사실 자바스크립트의 부분이 아니다.

브라우저에서 사용을 한다면, 브라우저 API 를 사용하는 것이며 ( window object),
Node 에서 사용한다면 Node API 를 사용하는 것이다. ( global object)

 

결국, 자바스크립트는 비동기처럼 사용할 수 있다! 하지만 이는 다른 것의 도움을 받아서 비동기처럼 사용하는 것이다! 

 

그럼, 내부에서는 어떻게 진행될까?

아까처럼 아래 소스코드가 자바스크립트와 Web API 를 사용해서 어떻게 진행되는지를 살펴보자.

console.log('1');

setTimeout(() => {
    console.log('2');
}, 3000);  // 	3초 이따가 실행

console.log('3')

 

이들을 다 이용해서 소스코드를 처리하는 것이다.

 

1) 자바스크립트 엔진

- 자바스크립트 코드를 실행하려면, 자바스크립트 엔진이 필요하다. 그리고 엔진은 두 가지 주요 구성요소로 구성된다.

 

1) 메모리 힙

  • 메모리 할당이 발생하는 곳이다. (변수를 정의하면 저장이 되는 창고)

2) 호출 스택

  • 코드가 실행될 때 스택들이 이곳에 쌓이게 된다.

콜 스택

이 때, 더 이상 실행할 함수가 없다면 호출 스택이 비게 된다.

 

※ Call Stack 작동하는 원리

 

- 아래에 있는 소스코드를 사용해서 Call Stack 에서 어떻게 진행됐는지 살펴보겠다.

function B() {
    setTimeout(function () {
        console.log('B-1...');
    }, 1500);
}

function A() {
    console.log('A-1...');
    B();
    console.log('A-2...');
}

A();

 

위 코드처럼, A 라는 함수와 B 라는 함수가 있다. 그리고, A 를 먼저 호출하면, B는 A 함수 내부에서 호출이 된다.

가장 먼저, "A-1..." 이 출력되고 B함수가 호출되지만, 1.5초 이우헤 "B-1..."가 출력되도록 세팅해놨기 때문에, "A-2..."가 먼저 출력되고, 나머지 "B-1..." 이 출력된다.

A-1 -> A-2 -> B-1

 

여기서 Call Stack이 어떻게 하나하나씩 쌓이면서 동작하는 것인지 그림을 통해 확인해보자.

 

가장 먼저, 함수 A가 호출이 되면 call stack에 A함수가 들어오게 된다. 그리고 console.log('A-1...') 부분이 들어오는데, 이게 출력이 되면 call stack에서 그 부분은 사라지게 된다.

그런 후에, 함수 B가 호출되고 call stack에 B함수가 들어온다. 그 다음, setTimeout() 부분이 들어가게 되는데 이 부분은 비동기 부분인데, 얘는 출력이 되지도 않았는데 call stack에서 사라지게 된다. (어딘가 다른 곳으로 우선 갔다고 생각하면 된다.)

그렇게, setTimeout() 부분이 없어져서 B함수는 할 일이 없어져서 사라지게 되고, 다시 A에서 console.log('A-2...') 부분이 들어오게 된다. 이 부분도 역시 출력된 후 없어지고 이젠 A함수도 할 일이 없어져서 사라진다. 

근데 갑자기 아까 없어졌던  console.log('B-1...') 부분이 다시 call stack 으로 들어오게 된다. 그 후, 이 부분도 출력이 되고 없어지는데, 그럼 이 setTimeout의 함수가 어디로 갔다가 다시 왔는지를 보면 된다.

 

이 부분은 비동기 부분이라 동기 언어인 자바스크립트는 이를 처리하기 위해 브라우저의 도움을 받아야 한다. 브라우저에서 제공해주는 웹 API 들이 있는데, 여기서 비동기를 처리해주는 것이다.

 

setTimeout 이나 document, 윈도우 객체 안에 있는 것들이나 AJAX 등의 요청을 보낼 때, 이런 것들을 다 브라우저의 도움을 받아서 해주는데 아까 중간에 없어진 부분이 사실 이 웹 API 쪽으로 보내진 것이다. 

웹 API에서 코드상에 나타난 1500ms(1.5초)를 기다린 다음에 위 설명처럼, call stack 말고도 callback queue가 있는데, 이를 콜백큐에 보낸다. 시간이 다 되면, 콜백 큐로 함수가 들어오는데, 이 콜백 큐에는 웹 API 의 함수들이 대기해 있다.

 

즉, callback queue에서 대기를 하다가 event loop 에서 call stack이 비게 된 순간 먼저 들어온 순서대로 콜백 큐에 있는 대기함수들을 call stack으로 옮겨준다.

 

이런 과정을 시각적으로 확인해보고 싶다면, 아래 사이트에서 함수를 작성해 직접 과정을 볼 수 있다.

http://latentflip.com/loupe/

 

http://latentflip.com/loupe/

 

latentflip.com

https://kamronbekshodmonov.github.io/JELoop-Visualizer/

 

JELoop Visualizer

 

kamronbekshodmonov.github.io

 

 

그럼 이제 또 궁금한게 생길 수 있다. 

" setTimeout 의 지연시간이 0이라면?  어떻게 되는데? " 지연시간을 0초로 세팅한다면, 즉시 실행되는 것이 보장될까?

console.log('1');

setTimeout(() => {
    console.log('2');
}, 0);  // 	0초로 세팅

console.log('3')

 

실제로 위 사이트에서 실험을 해보면, 그렇지는 않다.

 

setTimeout 함수가 web API에 온다음에 콜백큐에서 대기할 때, 콜스택에 어떠한 것이 있으면 계속 대기하다가 event loop가 콜스택이 비게 되면 그 다음에 콜백큐에서 대기하고 있는 것을 콜스택으로 전달하는 로직이라고 앞서 말했다.

즉, 0초 이후에 setTimeout 함수가 호출되는 것을 보장하는 거지(호출돼서 webAPI로 가는것) 0초가 지난 다음에 콜백함수를 호출하는 것(콜백큐에 있는 함수가 호출되는 것) 을 보장하는 것이 아니기 때문이다. 똑같이 console.log('1...') -> console.log('3...')  -> console.log('2...') 순으로 출력되겠다. 

 

※ 마지막으로, call stack 사이즈가 초과되는 경우도 유의해야 한다. 아래와 같이 코드를 짜면 에러가 난다.

function foo() {
    foo();
}
foo();

콜 스택 overflowing

쉽게말해 끝없는 재귀 함수 호출이 반복되는 되는 셈이다.

maximum call stack size exceeded


참고 자료

따라하며 배우는 자바스크립트 A-Z

반응형