[JavaScript] Javascript 핵심개념 알아보기 - 데이터타입, 실행 Context, this

2020. 8. 2. 19:03강의/JS Flow

부스트 캠프라는 활동에 참여하게 되어, 기존에 하던 자바언어 공부를 잠시 멈추고 

자바스크립트 공부를 시작하게 되었다. 

학교에서 c언어를 배우고 c++를 배우고 이어서 자바를 배우고...

새로운 언어를 배울 때마다 새로운 문법과 표현방식, 코드 컨벤션 등을 익히는데 시간을 들였다.

하지만 위의 언어들은 문법과 표현방식만 달리하면 평소하던 코딩 스타일을 유지할 수 있었다.

처음엔, 자바스크립트 또한 표현방식과 문법만 익히면 쉽게 코드를 짤 수 있을 거라 생각했다.

하지만... 자바스크립트는 정말로 이상한 언어라는 생각이 들었다. 

매우 유연하게 만들어졌지만 그만큼 안정성 보장이 안되고(타입스크립트가 나오긴 했지만...)

코드의 컴파일과정부터 실행되기까지가 싱글 스레드로 돌아가기 때문에 해당 로직도 이해해야 했다.

어떻게 공부하면 좋을까 고민 중에 추천받은 강의가 있어서 들으며 정리하게 되었다.

 

1. 데이터 타입

Primitive Type과 Reference Type을 구분하는 이유에 대해 배웠다.

먼저, Primitive Type의 선언과 할당이 메모리적으로 어떻게 이루어지는지 살펴보면,

1) a라는 변수를 선언하면 그 a를 위한 메모리 공간을 정하고 이름값을 넣어준다. (이름값은 편의를 위한 용어)

2) a='abc'; 가 이루어지기 위해서, 'abc'라는 값을 5004번 주소에 넣는다.

3) 값이 할당될 a의 주소에 찾아가서 값으로 5004의 주소를 넣어준다. 

4) 이후에 a='abcdef'가 실행될 때  'abcdef'를 위해 5005번 주소가 사용되었고, 1003번 주소에서 값을 5005의 주소로 바꾸어준다.

 

Reference Type의 선언과 할당은 위에서 한 단계를 더 거친다.

1) 선언부를 먼저 처리하기 위해 1002번 주소에 이름 obj를 넣어준다.

2) 할당 부분을 처리하려고 보았더니, 들어가야 할 정보가 한 가지가 아니다. 즉, obj라는 변수가 여러 데이터를 가져야 한다.

이때, 메모리 한 칸은 하나의 데이터만을 가질 수 있으므로, obj가 갖는 데이터들을 위한 주소인 5002번 주소에는 

@7103 ~  즉, 7103부터 하나하나 데이터를 저장한 값을 가리키는 값이 들어간다.

3) 들어가야 할 데이터가 처음 추가되는 데이터의 이름이 a이므로 7103 주소의 이름은 a가 되고, a에 저장될 1이라는 숫자는 또 다른 메모리에 저장되어 그 주소 값이 7103의 값으로 들어온다.

4) 다음으로 들어갈 데이터인 b:'bbb'를 저장하기 위해 7104 주소는 이름을 b로 하고, 저장될 데이터 'bbb'를 위한 주소를 따로 할당한 후 그 주소를 값으로 갖는다. 

5) obj.a=2;가 실행되는 원리는 obj를 담은 1002번째 주소에 접근 >> 5002번 주소 >> 7103부터 이름이 a인 주소를 찾는다. >> 7103 주소에 접근, 새로운 값이 부여된 주소로 값을 변경 @5003 -> @5005

 

Primitive Type과 과정이 비슷해 보이지만, 결정적인 차이는 obj의 주소 값에 할당된 값 : @5002가 변화되지 않았다는 것이다. 

이것이 가능한 이유는 데이터를 저장하는 메모리 부여가 한 단계 더 이루어졌기 때문이다. 

 

아래의 사진을 말로 설명해보는 연습을 해봐야겠다.

+)  primitive변수가 주소에 할당되면 그 값은 계속해서 재사용된다. 즉 , x:3이나 obj.arr [0]은 같은 5003번 주소를 가리키게 된다.

 

+) 두 객체가 같은 주소를 가리키면, 불변성을 보장하기 힘들어진다. 따라서 매번 새로운 객체를 만들자는 패러다임이 등장했다.

 

2. 실행 Context

실행 콘텍스트는 어렴풋이는 이해할 수 있지만, 디테일하게 이해하는 데에 어려움을 겪었던 주제이다.

먼저, ES6 이전에는 자바스크립트에서 block scope개념이 없었기 때문에 if문이나 for문 내에서 선언한 변수를 밖에서 사용할 수 있는 희한한(?) , 그 당시에는 당연했던 현상이 벌어진다. ES6에 오면서 let과 const키워드가 생기면서 block scope 개념이 적용될 수 있게 되었고, 이것은 자바에서 넘어온 나에게 정말 꼭! 알아야 할 사항이었다. ( var과 let의 차이!!)

 

실행 Context는 한마디로 함수를 실행할 때 필요한 환경정보! 로 정리 되었었는데, 이것은 여전히 추상적이므로 아에 객체로 만들어 버렸다고 한다. 즉, 함수를 실행할때 필요한 환경정보를 담는 객체가 실행 Context로 정의되게 된 것이다.

한편, 실행 Context를 이해하기 위해서는 알아야 할 몇 가지 개념이 있다. 

먼저, Call Stack이라는 개념을 살펴보자.

Call Stack은 함수들이 쌓이는 하나의 구조라고 볼 수 있는데, stack의 성질을 가지고 있다. 즉, 가장 늦게 호출된 함수일수록 Call Stack의 상단에 위치하게 된다. 이러한 성질 덕분에 Call Stack의 가장 위에 존재하고 있는 함수는 현재 실행 중인 함수임을 알 수 있다. 또한 다음에 호출될 함수가 무엇인지도 알 수 있다.

사진의 왼쪽이 call stack이라고 생각해보자.

1) 전역 콘텍스트는 js파일이 실행되는 그 순간 콜 스택에 담기게 된다. 

2) 이후에 outer함수를 호출함으로써 call stack에 쌓이게 된다.

3) outer함수 실행 중 inner() 함수를 호출하기 때문에, 즉 outer함수가 끝나기 전에 inner함수가 호출되었으므로 call stack에 쌓인다.

여기까지가 현재 위의 그림에 대한 설명이다. 이후에 벌어질 상황을 예측해보면,

4) inner함수의 실행이 끝나면서 call stack에서 pop이 된다.

5) outer함수가 call stack의 가장 상단, 즉 지금 실행되는 함수가 되었으므로 inner() 함수 호출 밑에 부분을 쭉 실행하고 pop 된다.

6) 파일 내의 코드가 모두 처리되면 전역 콘텍스트도 pop 됨으로써 실행할 모든 함수를 실행 완료한 상태가 된다. 

 

그렇다면 콜 스택에 쌓이는 각 함수 콘텍스트의 구체적인 모습은 어떻게 생겼을까?

inner함수만 대표적으로 살펴보면, 크게 VariableEnvironment와 LexicalEnvironment로 나뉜다. 

VariableEnvironment의 스냅숏 개념은 최초의 상태를 사진 찍듯이 남겨두는 것이고, 변화하는 것은 LexicalEnvironment이다. 

각 Environment에는 다시 environmentRecord와 outerEnvironmentReference가 있다.

environmentRecord는 우리가 흔히 알고 있던 hoisting을 뜻한다. hoisting으로 알고 있던 개념은 사실 허구의 개념이고, environmentRecord에 데이터를 수집하는 과정이 hoisting현상의 구체적인 동작 과정이라고 볼 수 있다. 

굉장히 어렵고 헷갈리지만, 내가 이해한 바로는 environmentRecord에 데이터를 수집하기 위해서 각 콘텍스트의 식별자 정보를, 가장 상위의 콘텍스트로 끌어올리는 것이다. 그리고 이 과정은 해당 콘텍스트가 실행될 때 가장 먼저 하는 작업이다. (구체적으로 한줄한줄 실행하기 전에, 먼저 식별자들을 environmentRecord에 기록하는 일이 발생한다는 것이다.)

위 사진에는 a, b, c라는 식별자가 있다. 함수 식별자의 경우 특이한 케이스로서, 함수 그 자체를 최상의 콘텍스트로 끌어올린다. 

위의 호이 스팅이 끝나면 가장 상단에는 function a와 var b, var c가 올라갈 것이다.

즉, 실제로는 왼쪽처럼 보이지 않는 곳에서 동작하지만, 이것을 이해하기 쉽게 코드로 표현하자면 오른쪽과 같이 되는 것이다. 

 

... 정리를 하면서도 굉장히 난해하다... 어렴풋이 이해는 하지만, 제대로 설명하라고 한다면 할 수 있을지 모르겠다 

environmentRecord에 호이 스팅 된 데이터가 기록이 되는 곳이라면, outerEnvironmentReference는 해당 콘텍스트에 관련되어있는 외부 컨텍스트에 대한 참조 정보이다. 아래와 같이 outer의 LexicalEnvironment에 대한 정보를 inner의 outerEnvironmentReference를 통해 참조하고 있는 것이다.

이 개념에 의해, scope chain이 이루어진다. 즉, inner의 environmentRecord의 경우, 어디에서도 참조하고 있지 않으므로 inner함수 내에서만 사용되지만, outer의 environmentRecord에 기록되어있는 데이터의 경우, inner의 outerEnvironmentReference에서  참조하고 있으므로 inner함수 내에서도 사용될 수 있게 되는 것이다. 

 

결국 위와 같이 각 함수에서 접근할 수 있는 식별자들의 범위가 정해진다. 

 

위에서 잠깐 언급한 scope chain이란, inner에서 어떤 식별자를 사용하기 위해서는 먼저 자신의 environmentRecord를 탐색한 후, 존재하지 않으면 outer의 environmentRecord를 탐색하고, 없으면 전역의 environmentRecord를 찾는 일련의 과정이다.

예를 들어서, inner, outer, 전역에 모두 var a가 선언되어 있고 각각 1,2,3이 할당되어있다고 생각해보자.

각 함수에서는 먼저 자신의 environmentRecord를 탐색하여 존재한다면 해당 값을 사용하고 없다면 다음 참조 콘텍스트의 environmentRecord를 탐색한다. 결과적으로 각 함수에서 a를 출력하면 1,2,3이 나온다.

만약, outer와 전역에만 var a가 선언되어있고 각각 2,3으로 할당되어있다면? inner에서 자신의 environmentRecord에 a가 없는 것을 확인하고 다음 참조 콘텍스트인 outer에 가서 a를 탐색하는 것이다! 결과적으로 2,2,3이 출력될 것이다.

 

아래의 사진을 꼭 이해하자!

 

3. this

자바스크립트 공부를 시작한 지 일주일 정도밖에 안되었지만, 가장 헷갈리는 것을 꼽으라면 꼭 하나 말하고 싶은 것 중 하나가 바로 this에 대한 개념이다. 당연히 위의 내용을 모르기 전엔 이 this라는 개념을 헷갈릴 수밖에 없었다. 하지만 위의 내용과 연계하여 이해하면 어렵지 않다고 하니 살펴보자.

위에서 한 함수가 콜 스택에 들어갔을 때 그 구체적인 정보로 Lexical Environment에 대해 살펴보았다. 이때 담겨있는 정보중 thisBinding이라는 것에 대해 알아볼 것이다. 즉, thisBinding은 실행 콘텍스트가 활성화 될때 한다는 것이다 ! (그리고 실행컨텍스트가 활성되는 시점은 그 함수가 호출되었을 때이다. 그렇다면 this가 담겨있는 그 콘텍스트를 호출한 주체가 해당 콘텍스트의 this가 되는 것이다!.... 헷갈린다..)

 

근데? 

 

c안에 this는 그렇다면 c를 호출한 주체인 b가 c안에서의 this가 되어야 하니깐 this는 b가 될 것 같지만...............

window 즉 전역 객체를 가리킨다...? 이것에 대해 자바스크립트의 실수다 vs 자바스크립트의 하나의 특성으로 받아들여야 한다 라는 의견이 분분하다고 한다. 이러한 문제 때문에 ES6부터는 this바인딩을 하지 않는 arrow function이 나오게 되었다. 즉, arrow function에서는 위와 같은 경우 c의 this가 b가 되는 것이다. 위에서 이해한 것을 그대로 적용할 수 있는 것이다!!!

 

+ 위에 것들이 다 헷갈린다면 호출된 형태를 보는 것이 가장 쉽다고 한다. 즉, this가 담긴 콘텍스트를 호출하는 형태가 함수라면 this는 window/global이 된다고 외운다! (함수는 전역 객체의 메서드라고 외운다. 그리고 호출방식이 메소드면 .앞의것이 this가 된다고 외운다.)

 

위 예제를 예측해보자.

먼저, this는 obj의 메소드 b에서 한번, 그 b메서드의 내부 함수 c에서 한번 불러진다.

호출 방식만 보자고 했다.

첫 번째 this의 경우 obj.b() 즉, 메서드를 호출하는 방식이다. 그렇다면 이때 this는. 앞의 obj가 되므로, this.a는 obj의 a가 되어 20이 출력이 된다.

두 번째 this의 경우 c() 즉, 함수를 호출하는 방식이다. 함수를 호출하는 방식에서의 this는 global객체를 의미한다고 하였으므로 이때의 this.a는 전역 객체의 a가 출력된다 

+) 10이 나올거 같았지만 undefined가 나와서 이유를 찾아보았다 .

www.inflearn.com/questions/47466

 

흠... 코드의 흐름상 c함수에서도 this.a가 20이길 원할 확률이 높은데... 그렇다면 이것이 b메서드의 this와 같은값을 갖게하려면 어떻게 해야할까 !? 해당 강의에서는 우회방법을 알려주었다.

바로 b메서드 안에서의 this를 다른변수에 담아서 c함수에서는 해당변수로 접근하는 것이다. 

이렇게 self는 메소드 호출 방식 안의 this를 self라는 변수에 담는다. ( this는 obj객체를 가리키게 된다.) 

그리고 c함수에서 해당 변수로 접근을 한다. ( 위에서 배웠던 것처럼 c함수 콘텍스트에서는  자신의 environmentRecord를 탐색한 후 자신을 호출한 곳을 탐색하게 되어 결국 self가 obj를 가리킬 수 있게 된다!!

 

콜백 함수에서의 this...

위에서 호출 방식이 함수형태일때, 메소드형태일때를 보았다. 그렇다면 this키워드가 콜백함수내부에 들어있을때, 해당 콜백함수를 넘겨받은 함수에서 실행이되면 this는 무엇을 가르킬까 ?? 일반적으로는 그냥 호출방식이 함수일 때와 같다고 한다. 

 

 즉 위에서 this가 들어있는 함수를 호출하는 방식이 함수 호출 방식이므로, this는 전역 객체 (node상에서 global)을 가리키게 된다!

 

호출방식에 따른 this의 의미

코딩 인생의 고비가 this로 찾아올 줄은 몰랐다...