개발/JavaScript

자바스크립트가 비동기를 가능케하는 원리에 대해 궁금하여 공부하다가 잃어버릴까봐 정리 해논 글입니다.


현 회사에서 자바 RestAPI  서버단 코딩을 주로 하다가 자바스크립트로 클라언트 코딩할 일이 생겨.... 작업중 


브라우저에 대한 단일 쓰레드 비동기 처리방식이 어떻게  가능케 하는 원리가 궁금하여... 검색을 찾고 공부한 내용을 적어 둔 내용입니다.


출처는 http://meetup.toast.com/posts/89 입니다.


내용을 공유하여 주신분께 정말 감사하게 생각하고 있습니다..



1. 나의 의문점


어떻게 스레드가 하나인데 '자바스크립트는 어떻게 동시성(Concurrency)을 지원하는 걸까'?


자바는 쓰레드로 넘기면 되지만 단일 쓰레드에서 어떻게 저런일이 일어날까.....??


저의 의문점으로 시작하여 검색키워드에 '자바스크립트 비동기 원리' 검색을 시점으로 여러글을 읽었지만 Promise 객체 사용법에 대해서만 있었다.


몇몇 우연치 않게 Event Loop 라는 용어와 비동기 처리는 setTimeout 함수 및 setInterval로 사용할 수 있다. 라는 힌 검색 키워드를 알아 냈고


크롬에 개발자 모드에서 Call Stack부분을 봐도 setTimeout 함수는 Call Stack에서 사라지고 다른 함수들이 


Call Stack쌓이는 관경을 목격..  스택에 setTimeout  함수는 없는 어떻게 호출이 되는거지에 대한 의문에 시작


출처 사이트를 보면서 의문점에 대한 해갈을 시작함...



2. 출처를 보고 내린 결론


이게 결론임 자바스크립트가 '단일 스레드' 기반의 언어라는 말은 '자바스크립트 엔진이 단일 호출 스택을 사용한다'는 관점에서만 사실이다.


아래 이미지가 정말 핵심 이미지다 저걸 이해해야 자바스크립트의 비동기 작동원리가 이해된다고 생각한다.



그림(1)






지금 그림(1)을 설명하면 


1.자바스크립트 엔진, 

2.웹 APIs, 

3.이벤트 루프 ,

4.Task 큐 

위  4가지가 별게인거다


보면 이벤트 루프가 돌면서 Task큐에 첫번째큐를 실행 Task 큐안에 콜백함수 CallStack들이 실행 되어지는것이다.


setTimeout 이나 setInterval ,Ajax 함수는들을 호출하면 콜백함수가 바로 지금 실행중인 Task 큐에 CallStack에 들어가는 것이 아닌


다음 Task 큐에 들어간다 결국 다른 Task들인 셈이다. 각 Task별로 각 CallStack이 있는 개념으로 생각 하는것이 맞을 것이다.


이런 원리 때문에 자바스크립트가 비동기로 돌아가는 원리로 돌아 가는것이다


콜스택개념은 전 포스트에 남겨놓았으니 확인하기 요망



'개발 > JavaScript' 카테고리의 다른 글

비공개 자바스크립트 콜스택 보는법  (0) 2016.11.03
개발/JavaScript
출처를 먼저 밝힙니다.

http://cafe.naver.com/hacosa 사이트에서

[ES5] 콜 스택과 스코프에 대한 고찰
 제목으로


엑소버드님이 쓰신글을 공부용으로 복사했습니다. 비공개 처리합니다.



저는 하나를 파고들면 끝장을 보는 성격이라 콜스택과 스코프가
말로만 듣던 것과 동일하게 작동하는지 궁금해서 한번 그 내부 구조를 들여다보았습니다~

먼저 자바스크립트에서 실행 컨텍스트, 클로저, 스코프, 콜스택 등등에 대해서
이 글에서 자세하게 설명하지 않습니다.
구글에서 검색해보시면 아주 많이 나옵니다.
이 글은 '아, 브라우저에서 정말 그렇게 돌아가구나' 라는 걸 확인하기 위한 글입니다.
콜 스택 === 실행 컨택스트 스택
정도로 보시면 될 것 같습니다.

다른 브라우저는 모르겠고, 크롬이 주 브라우저고, 크롬에서 디버깅하기가 용이하여

부득이하게 크롬으로 진행해보았습니다~

'use strict';
var A = function() {
var A = 1;
var AA = function() {
console.log(A);
};
AA();
};

A();

위와 같이 외부 함수 A와 내부 함수 AA가 있을 때
외부 함수 A를 실행하면 스코프가 어떻게 형성되고, 콜 스택은 어떻게 변화하는지 보겠습니다.


크롬 개발자 도구에서 소스 탭으로 들어간 후에 브레이크 포인트를 아래와 같이 걸어보고,
프로그램을 돌려봅니다.

콜 스택 맨 아래에 있는 익명함수는 전역 스코프의 실행 컨택스트입니다.



ES5에서 전역 스코프에서 var로 선언한 변수는
Global 객체(브라우저에서는 Window 객체)의 프로퍼티로 들어갑니다.
window.A = 123 등으로 변경이 가능하지만,
delete 연산자로 삭제가 불가능하므로 유사 프로퍼티(?)라고 저는 보고 있습니다.
그리고 2번 라인이 실행되기 직전에 브레이크가 걸렸으므로 A는 아직 undefined입니다.


F8 버튼을 눌러 다음 브레이크 포인트로 이동하면 10번 라인에서 걸립니다.
변수 A에는 함수가 할당된 걸 볼 수 있습니다.


F8을 눌러 다음 브레이크 포인트로 이동하면 10번에서 함수 A를 호출했으므로
함수 내부인 3번으로 들어온 후 브레이크 됩니다.
콜 스택에 함수 A의 실행 컨텍스트가 쌓였습니다.
그리고 스코프 체인에는 Local 객체(현재 제어권을 가지고 있는 함수(A)의 실행 컨택스트 내부의 변수 객체)
가 Global 스코프보다 위로 왔습니다.
아직 지역변수 A와 AA에는 값을 할당하기 이전이고, ES5의 Strict 모드에서 함수에 this를 명시해주지 않으면
this에는 undefined가 할당됩니다. this를 명시해주고 싶다면,
10번 라인과 같이 함수를 호출할 때 call, apply, bind 등의 메소드를 사용하시면 됩니다.


다음 브레이크 포인트로 이동하면 4번 라인에서 걸립니다.
3번이 실행된 이후이므로 지역변수 A에는 1이 할당됨.


그 다음 브레이크 포인트로 이동하면 7번에서 걸리고,
지역변수 AA에는 함수가 할당된 것을 볼 수 있습니다.


그 다음 브레이크 포인트로 이동하면, 7번에서 함수 AA를 호출했으므로
AA 함수 내부인 5번 라인에서 브레이크가 걸립니다.
콜스택을 보면 제일 나중에 실행한 AA 함수의 실행 컨택스트가 제일 위에 쌓여 있고,
그 아래 A 함수의 실행 컨택스트가 쌓여있습니다.
AA 함수의 실행 컨택스트의 스코프 체인을 봅시다.
로컬 스코프를 보면 역시 this를 명시해주지 않았으므로 this에는 undefined가 바인딩 되고,
그 아래 Closure (A) 스코프와 전역 스코프가 있습니다.

이 쯤에서 그럼 클로저에 대한 정의를 다시 한번 살펴봅시다.

인사이드 자바스크립트 책에서는...
이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수
라고 정의하고 있고, 한 블로그( http://blog.javarouka.me/2012/01/blog-post_13.html ) 에서는
생성 당시의 스코프에 대한 연결을 갖는 블록
이라고 정의하고 있습니다.



빨간색 부분이 이미 생명 주기가 끝난 외부 함수, 생성 당시의 스코프이고,
파란색 부분 외부 함수의 변수를 참조하는 함수, 연결을 갖는 블록이 된다.

함수 AA(클로저)의 실행 컨택스트의 스코프 체인은
AA 자신만의 지역 스코프를 가지고,
또 5번 라인에서 자신의 스코프를 벗어난 변수 A를 참조해야 하므로
이미 생명 주기가 끝난 외부 함수를 참조하는 스코프를 생성했다. (Closure (A))
이렇게 클로저를 많이 쓰다보면 스코프 체인에 여러 스코프가 생기고,
이는 로컬 스코프만 가지는 함수보다 더 많은 메모리 공간을 차지함으로써
메모리 누수와도 연결이 되므로 적절하게 써야합니다.


다음 브레이크 포인트로 이동하면 함수의 실행을 마치고
함수 구문의 끝인 6번 라인으로 이동합니다.
이 때 로컬 스코프를 보면 Return value: 라는 프로퍼티가 생기는데
return을 명시하지 않았으므로 undefined가 반환됩니다.


다음 브레이크 포인트로 이동하면 콜 스택에서 AA 함수의 실행 컨택스트가 반환(pop)된 걸 볼 수 있습니다.
함수 A의 끝인 8번 라인에서 브레이크가 걸리고, 역시 Return value가 생기고
return 값을 명시하지 않았으므로 undefined가 반환됩니다.

그 다음 브레이크 포인트로 이동하면 브레이크 걸리는 것 없이 프로그램이 끝납니다.
10번 라인에서 A함수를 호출했고, A함수의 호출을 마쳤기 때문이죠.

이번에는 다른 예제를 한 번 봐보겠습니다.
'use strict';
var B = function() {
var A = 1;
C();
};

var C = function() {
console.log(A);
};

var A = 2;

B();

함수 B와 함수 D가 있고 전역 스코프에 A라는 변수가 있고,
함수 B는 지역변수 A를 가지고 있고, 함수 C를 호출합니다.
함수 C는 참조할 수 없는 변수인 A를 콘솔창에 찍는 함수입니다.
마지막으로 함수 B를 실행하고 끝납니다.
브레이크 포인트를 아래와 같이 걸어줍시다.

그럼 함수 B를 호출하는 13번 라인에서 브레이크가 걸립니다.


그 다음은 함수 B 내부인 4번에서 브레이크가 걸립니다.
콜스택에는 B가 쌓여있고, 스코프에는 B 함수의 로컬 스코프와 전역 스코프가 있습니다.
로컬 스코프의 로컬 변수 A는 1이고, 글로벌 스코프의 글로벌 변수 A는 2입니다.
이제 다음 브레이크 포인트로 이동해서 함수 C를 호출해봅시다.


함수 C 호출 한 후에 8번 라인에서 브레이크가 걸렸습니다.
콜스택에는 B 위에 C가 Push 되었고,
스코프를 보면 C의 로컬 스코프, 글로벌 스코프가 있고, 글로벌 스코프에 A 변수가 있습니다.


그리고 다음 브레이크 포인트로 이동하면 C의 실행 컨텍스트가 스택에서 Pop 됐습니다.
그리고 콘솔탭으로 이동하면 2가 찍혀있습니다.
1이 찍힐 것이라고 예상하신 분들은 아래를 보시면 되겠습니다...

ES5에서 변수의 스코프는 함수 단위입니다.(ES6의 let과 const는 블록 스코프!)

첫 번째 예제를 도식화하면 좌측과 같고, 두 번째 예제를 도식화 하면 우측과 같습니다.



마지막으로 전형적인 클로저 활용 방안(?)에 대해서 정리하고 끝마치겠습니다.
여러가지가 있지만 클로저를 이용하여 콜백 함수에서 반복 변수(?) i를 묶어보도록 하겠습니다.

'use strict';
for(var i=1; i<=3;) {
setTimeout(function() {
console.log(i);
}, i++ * 1000);
}
콜백 함수는 이벤트 핸들러 등록할 때 함수를 실행하는 게 아니라,
등록만 해두고 이벤트가 발생했을 때 이벤트 핸들러가 실행하는 점을 유념하셔야합니다.
setTimeout 함수의 콜백함수로 익명함수를 등록해놓고, 1초가 지날 때마다 로그를 찍습니다.
1
2
3
이 찍히리란 걸 예상할 수 있는데, 어떨지 한 번 봐야할 것 같습니다.


반복문 1회차 전역 스코프의 i의 값은 1.
i++ * 1000(ms), 즉 1초 후에 이벤트 핸들러를 실행하라고 합니다.



반복문 2회차 전역 스코프의 i의 값은 2.
i++ * 1000(ms), 즉 2초 후에 이벤트 핸들러를 실행하라고 합니다.



반복문 3회차 전역 스코프의 i의 값은 3.
i++ * 1000(ms), 즉 3초 후에 이벤트 핸들러를 실행하라고 합니다.

반복문 4회차에는 전역 스코프의 i의 값은 4이고 3보다 크므로 false.
반복문을 탈출합니다.


1초가 지났으므로 이벤트 핸들러가 동작합니다.
i를 콘솔 탭에 찍는데 현재 전역 스코프의 i의 값은 4입니다.


2초가 지났으므로 이벤트 핸들러가 동작합니다.
i를 콘솔 탭에 찍는데 현재 전역 스코프의 i의 값은 4입니다.


3초가 지났으므로 이벤트 핸들러가 동작합니다.
i를 콘솔 탭에 찍는데 현재 전역 스코프의 i의 값은 4입니다.

결과적으로
4
4
4
가 찍힙니다.
저희가 예상한 결과와는 완전 딴판이 나왔습니다.
이벤트가 몇 초 후에 동작해라~ 라는 것은 정확히 동작하지만
콜백 함수는 등록 시점이 아닌 실행 시점의 i를 참조하므로
i를 묶어둘(복사해 둘) 필요가 있습니다.

이 때 사용하는 게 바로 클로저,
상위 스코프에 i를 복사해두고
하위 스코프(이벤트 핸들러)에서 상위 스코프의 복사된 i를 참조하게 하는 기법입니다.

도식화 해보자면 아래와 같습니다.


'use strict';
for(var i=1; i<=3; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}(i));
}


콜스택에 빨간 화살표시 한 익명함수의 실행 컨택스트가 쌓입니다.
그리고 로컬 스코프를 보면 전역에 존재하는 현재 반복문의 i의 값을 복사했습니다.


2회차
i의 값인 2를 또 복사함.



3회차
i의 값인 3을 또 복사하고 반복문이 끝납니다.


1초 후...
콜스택에 익명함수의 실행 컨택스트가 쌓였고
이 실행 컨택스트는 1초 후 실행되는 이벤트 핸들러 함수의 실행 컨택스트입니다.
그리고 스코프를 보면 로컬 이외에 상위 스코프인 클로저가 생기고,
클로저에는 파란색 동그라미 쳐놓은 상위 스코프의 복사해놓은 i를 참조하게 됩니다.


2초 후...
역시 상위 스코프에 복사한 i값인 2를 참조합니다.


3초 후...
상위 스코프에 복사한 i값인 3을 참조합니다...

결과적으로 우리가 원하는 결과인
1
2
3
을 출력합니다.

다음의 예제에서 클로저가 있는지 없는지, 이유까지 한 번 생각해보세요.
'use strict';
var outer = function() {
var x = 2;
var inner = function() {
var y = 2;
console.log(y);
};
return inner;
};
var inner = outer();
inner();

즉시 실행 함수 예제도 한번 디버깅 해보세요.

'use strict';
(function() {
var outer = function() {
var x = 2;
var inner = function() {
console.log(x);
};
return inner;
};
var inner = outer();
inner();
}());
오늘 ES6의 스코프까지도 다뤄보려고 했는데
이 글 쓰느라 5시간 이상을 쓴 것 같네요.
이 범위 공부하는데만 이틀은 넘게 걸린 것 같고요...
ES6에서는 얼마나 달라졌을지 또 공부해봐야겠습니다~


'개발 > JavaScript' 카테고리의 다른 글

자바스크립트 비동기가 되는 원리  (0) 2017.02.24
개발/JAVA

자바 1.8 책을읽고....


자바 1.8이 나온지 엄청 오래됫는데.. 


VertX를 시작해보려 라이브러리를 까보는순간.. 1.8문법이 가득하여.. 1.8 책을 도서관에서 바로 빌렸다...


읽은 내용 정리 공간이다.



1장 람다



예전 자바에서 콜백은  파라미터에 인터페이스 객체를 넘겨서  실행하는 방법으로 객체를 넘기는 방식이 였다면


이젠 다른 언어처럼 코드블록 자체를 넘기는 방식이 가능 하다는게 책에 설명



1-1.코드에서 표현식 하나로는 표혈 할수 없는 계산을 수행한다면  (메서드처럼 작성)


1
2
3
4
5
(String first,String second)->{
           return Integer.compare(first.length(),second.length());    
};
 
cs



1-2).람다 표현식이 파라미터를 받지 않으면 파리미터 없는 메서드와 마찬가지로 빈괄호를 사용한다.


1
2
3
4
()->{
        return Integer.compare(5,10);    
};
 
cs


1-3). 람다표현식의 파라미터 타입을 추정할 수 있는 경우 타입을 생략할 수 있다.


1
2
3
4
Comparator<String> comp 
  = (first,second) //String first,String second와 같다
    -> Integer.compare(first.length(),second.length());
 
cs

1-4).메서드에 추정되는 타입 한 개를 파라미터로 받으면 괄호를 생략 할수 있다.


1
EventHandler<ActionEvent> listenver = event -> System.out.println("CLick");
cs


1-5). 결과 타입 또한 문맥으로 추정이 될수 있으면 리턴 값도 쓰지 않을수 있다.

1
2
3
4
5
(String first,String second)->Integer.compare(first.length(),second.length());    

 
cs



2 매서드 레퍼런스

1
button.setOnAction(System.out::println)
cs

1
2
3
4
button.setOnAction(System.out::println)
//= 같은 의미다.
button.setOnAction(x->{System.out.println(x)})
 
cs



3. 변수 유효범위


파라미터 구현된 인터페이스 객체를 넘겨서 사용하는 리스너 방식처럼 


람다식도 람다식을 감쌓고 있는 메서드에 파라미터를 접근하려면 final을 해야한다.


증감 연산자를 사용할수 없다.


람다안에서 this또한 일반 자바와 마찬가지로 그람다를 정의하는 class의 객체를 표현하는것이다.


4. 디폴트 메서드


이게 왜 생겼는지 궁금했는데.. 

책에는 Collection 인터페이스 forEach 같은 구현된 메서드가 필요 했다고 적혀 있다.

Collection을 상속받은 객체에 forEach를 여러번 반복된 소스를 구현하느니 .인터페이스에 디폴트 메서드를 만들어서 구현이 필요했나보다.

개인적 견해로 스트레이지 패턴으로 해결이 안되나?? 상속보단 인터페이스쪽을 바라보고 설계를 생각했기때문에 Collection을 구현한 객체들이 ForEach메소드를 중복되게 구현하는 문제 때문인가? 의구심이 들긴든다... 새로운 패턴을 정의할려면 기존방식을 흔들어야되는데 흔드니 새롭게 추가하는 부분이 안전할수 있을것 같다.


객체 여러 인터페이스를 구현 할수 있다. 

그러면.. 인터페이스나 다른 부분을 상속받은 메소드에 이름이 서로 같다면 어떻게 될것인가. 오버로딩이 아닌 파라미터 같지 같다면???


1) 상속을 하는 슈퍼클래스 메소드로 대체 된다. 인터페이스에 디폴트 메서드는 무시된다.


2) 인터페이스들끼리 같다면?? 해당 메서드를 오버라이드해서 충동을 해결한다???

    예제가 없어서 만들어 봤는데.  


1
2
3
4
5
6
public interface AirConditionFuction {
    public default String getName(){
        return "에어콘 ";
    }
 
}
cs


1
2
3
4
5
6
7
public interface FanFunction {
    public default String getName(){
        return "선풍기";
    }
}
cs



1
2
3
4
5
6
public class HeatKiller implements AirConditionFuction,FanFunction{
    @Override
    public String getName() {
        return FanFunction.super.getName();
    }
}
cs


AirConditionFuction ,FanFunction 

두개에 인터페이스중에 한개의 인터페이스에 디폴트 메소드만 구현하면 된다.


리턴되는 방식은 인터페이스명.상속객체.메소드명() 이런식이다.


(Object 클래스에 메서드중에 하나를 재정의하는 디폴트 메서드는 만들수 없다.)


1장 끝.

1 2
블로그 이미지

사람냄새나는 놈