오늘은 회사에서 혼이 났다.
정확히는 딴 부서 담당자에게 기분 나쁜 추궁을 당했었다.
?? : 여태까지 뭐하셨길래 지금와서 문제를 말하시는가죠?? 끝까지 집중합시다
흠녀... 그냥 뻘쭘해서 나에게 탓을 몰았다고 생각하고 있다.
그렇지만 기분이 안좋은 것은 변함이 없다.
기분 전환할 겸 자바스크립트나 배워보도록 하자
프로그래밍 언어의 가장 기초적인 문법인 변수이다.
컴퓨터는 모든 데이터를 2진수로 처리한다. 즉, 메모리에 저장되는 데이터들인 숫자, 텍스트, 이미지 , 동영상 등등 모두 2진수로 메모리에 저장되는 것이다.
다음과 같이 생긴 메모리에 변수들이 저장되는 것이다.
메모리는 크기에 따라 메모리 주소가 달라지는데, 4GB에서는 저렇게 표현된다.
console.log(10+20)
그럼 다음의 식을 쓴다면 10과 20은 메모리에 저장될까??
당근이다. 메모리에 다음과 같이 저장될 것이다.
물론 실제 메모리에서는 10과 20이 2진수 형태로 메모리에 저장되겠지만 이렇게 메모리에 저장되고, 자신의 메모리 공간이 있어 주소로 호출할 수 있게 된다.(물론 c/c++처럼 js는 주소를 개발자가 직접 가져올 수는 없다)
만약 직접 우리가 메모리 주소를 이용해서 값을 바꾸고, 변형한다고 생각해보자.
아주 끔찍하다. 실행할 때마다 바뀌는 메모리 주소 값은 어떻게 처리할 것이며, 잘못 처리했을 때 프로그램이 박살나는 문제를 어떻게 해결할 것인가?
그래서, 우리는 변수를 사용한다.
직접 메모리 주소에 접근할 수 없으니, 변수라는 것으로 메모리에 간접적으로 접근하는 것이다.
변수는 하나의 값을 저장하기 위해 확보한 메모리 공간 자체 또는 그 메모리 공간을 식별하기 위해 붙인 이름을 말한다.
즉, 변수는 메모리 주소에 저장된 값에 쉽게 접근할 수 있도록 사용하는 '도구'일 뿐이다.
바로 이렇게 말이다. 10과 20의 메모리 주소 번지를 직접 프로그래머가 입력하는 것이 아니라, a와 b 변수 이름을 두어서 10과 20을 간접적으로 불러오고 변형하는 것이다.
이렇게 변수는 프로그래머에게 메모리에 저장된 값에 쉽게 접근할 수 있는 도구 역할을 하는 것이다.
당신은 도구 입니다.
그럼, 이 도구인 변수가 어떻게 메모리 주소를 찾아갈까?? 그건 사실 우리가 알바가 아니다.
왜냐하면 컴파일러 또는 인터프리터에 의해 저장된 메모리 공간의 주소로 치환되어 변수가 실행되기 때문이다. 따라서 직접 개발자가 메모리 주소를 참고할 필요가 없다는 것이다.
변수를 만들어서 메모리 주소에 값을 넣는 방법은 여러가지가 있다. 변수 선언, 초기화, 할당 등이 있는데 용어가 헷갈릴 수 있으니 정리해보자,
변수 선언 : 변수 선언은 result와 같이 변수는 만들었는데 값은 넣지않은 경우, 변수만 있으니 선언이라고 한다.
초기화 : 초기화는 변수와 함께 값을 넣었으니 초기에 값을 넣어주었다라고하여 초기화라고 부른다. 그러나, 대에충 넓게 불러서 선언이라고도 말하는 경우가 있으니 딱딱하게 굴진 말자
할당 : 선언된 변수에 값을 넣는 것을 말한다. result와 같이 초기화가 안된 변수에 넣는 경우도 있고, a, b와 같이 초기화가 된 변수에도 할당은 해줄 수 있다.
이게 끝이다.
더 나아가서, result, a, b라는 변수들을 '변수 이름' 또는 '식별자'라고 한다. 더 나아가서, 변수말고도 함수나 클래스의 이름도 '식별자'라고 한다.
'식별자'는 메모리의 주소를 기억하고 있다. 즉, 메모리에 있는 값이 아닌 주소를 기억한다고 보면되고, 값을 넣는 로직은 컴파일러나 인터프리터가 대신 해준다고 볼 수 있다. 따라서, '식별자'는 메모리 주소에 붙은 이름과 같다.
tlqkf
변수를 선언하면 변수를 생성하는 일을 한다. 값을 저장하기 위한 메모리 공간을 확보하고, 변수 이름과 메모리 공간의 주소를 연결(name binding)해서 값을 저장할 수 있게 해준다. 변수 선언에 의해 확보된 메모리 공간은 해제되기 전까지 보호된다.
변수를 사용하려면 반드시 선언이 필요하다
그렇다면 선언해주기 위해서는 어떤 키워드들이 필요한가?? 그것이 바로 const, let, var 이다.
const result = 10;
let a = 10;
var b = 10;
const는 상수형을 위한 선언이다. 즉, 초기화 이외에는 값이 변하지 않는다.
var과 let은 변수 선언인데, 즉 값이 바뀐다는 것이다. 다만, ES5에 var의 단점이 부각되고 ES6에 단점을 해결한 let이 나오게 된 것이다. 물론 var을 써도 지금과 같은 초기 단계에는 크게 문제없으니 양껏쓰자.
여기서 궁금할 것이다. 안궁금해
let result;
console.log(result)
위 코드를 실행해보자
c/c++의 경우에는 쓰레기 값이라는 것이 들어가게 된다. 그럼 js역시도 쓰레기 값이 들어갈 것이다. 그러나, js에는 이를 보다 우아하게 undefined라는 자료형으로 표현한다.
다시 말한다. undefined는 자료형이다.
즉, 데이터 타입이라는 것이다.
아직 배우지 않았지만, js에는 string, 정수형, 실수형 등 다양한 데이터 타입들이 있다. undefined도 이러한 데이터 타입들 중 하나이고, 이들을 원시 타입의 값(Primitive value)이라고 부른다.
그럼 또 궁금한게 생기게 된다.
우린 'undefined'라는 값을 넣어주지 않았다.
그런데 어떻게 넣어준 것인가??
바로 자바스크립트 엔진이 해주는 것이다. 2단계에 걸쳐서 변수 선언을 수행하는데,
추후에 배울 예정이지만, 변수 이름을 비롯한 모든 식별자들은 자바스크립트의 실행 컨텍스트(execution context)에 등록된다. 이 실행 컨텍스트를 통해서 자바스크립트가 식별자를 스코프를 관리하는 것이다.
뭔가 그림이 복잡해보이지만, 간단하다.
let result라는 변수가 선언되면, 자바스크립트가 이를 '실행 컨텍스트'에 등록한다. 이것이 '선언 단계 착수'이고, 자바스크립트는 초기화 단계에 돌입한다. 값이 없는 경우에는 다음과 같이 undefined를 넣어주는 것이다.
변수 이름과 변수의 값은 실행 컨텍스트 내에서 key-value 쌍인 객체로 등록되어 관리된다. 지금은 자바스크립트 엔진이 변수를 등록한다고만 알아두자.
따라서, 만약 등록되지않은 변수, 식별자를 호출하려고하면 'ReferenceError'가 발생한다.
console.log(result)
다음의 코드를 실행하면
ReferenceError: result is not defined
요런 에러가 발생한다.
즉, 자바스크립트 엔진이 해당 식별자(여기서는 result 변수)를 실행 컨텍스트에서 찾았는데, 등록이 안되어있어 참조할 수 없다는 에러를 낸 것이다.
자바스크립트가 어렵고, 기묘한 언어인 이유 중에 하나인 것은 바로 우리의 머리와는 다른 구조로 실행된다는 것이다.
c언어를 생각해보자
#include <stdio.h>
int main(){
printf("%d",ret);
int ret = 0;
}
벌써 머리가 지끈하다. ret이 선언과 동시에 초기화가 되기전에 이미 printf를 통해 호출하고 있다. 에러가 날 것이 분명하다.
다음과 같은 코드를 js에서 똑같이 만들어보자
console.log(ret)
var ret = 0;
분명 선언되기 전에 호출하였으므로 ReferenceError가 발생해야할 것 같지만, 이상하게도 실행이되고, 0 이라는 분명한 값을 호출한다.
세상이 원망스러운 문법이다.
분명 js는 인터프리터로 작동하기 때문에, 소스코드를 한줄한줄 씩 읽고 실행하게 된다. 그렇다면 당연히 console.log(ret)이 var ret = 0 보다 앞에 있으니 먼저 실행될 것이고, 에러가 나야 정상 아닌가??
응 아니다.
사실은 이러한 과정이다.
변수 선언이 소스코드가 한 줄씩 순차적으로 실행되는 시점, 즉 런타임이 아니라 그 이전 단계에서 먼저 실행되기 때문이다.
자바스크립트는 런타임 단계(인터프리터로 코드가 실행)로 들어서기 전에 소스코드의 평가 과정을 거쳐 소스코드를 실행하기 위한 준비를 한다.
바로 요런 기가막힌 단계를 거친다는 것이다.
소스코드를 만들고, 우리가 실행 버튼을 누르면 '소스코드 평가 과정'에 돌입한다. 여기서 자바스크립트 엔진이 변수 선언을 포함한 모든 선언문(변수 선언문, 함수 선언문 등)을 소스코드에서 찾아내 먼저 실행한다.
'소스코드 평가 과정'이 끝나면 비로소 변수 선언을 포함한 모든 선언문을 제외하고 소스코드를 한 줄씩 순차적으로 실행한다.
console.log(ret)
console.log(sum)
func()
var ret;
var sum;
function func(){
console.log("this is function");
}
다음과 같은 코드가 있다고 하자,
var로 선언된 변수들과 function으로 선언된 func 함수들은 모두 식별자이고 '선언문'이다.
따라서, 실행 버튼을 누르면, 런타임으로 가기 전 '소스코드 평가 과정'에서 먼저 실행된다.
그럼 다음의 코드와 사실상 똑같은 상황이 되는 것이다.
var ret;
var sum;
function func(){
console.log("this is function");
}
console.log(ret)
console.log(sum)
func()
아주 기가막히고, 코가 막히다.
이처럼 변수 선언문이 코드의 맨 앞으로 끌어 올려진 것처럼 동작하는 자바스크립트 고유의 특징을 호이스팅이라고 한다.
호이스팅은 '끌어올리다'라는 의미로 해석하면 된다.
이는 변수의 선언 뿐만아니라, 위에 함수의 선언에서도 작동하는데,
var, let, const, function, function*, class
키워드를 사용해서 선언하는 모든 식별자(변수, 함수, 클래스 등)이 호이스팅의 대상이 된다.
주의: 식별자의 선언이 위로 호이스팅의 대상이 된다했지, 초기화가 식별자의 대상이 된다고는 안했다.
console.log(ret)
console.log(sum)
func()
var ret = 0;
var sum;
function func(){
console.log("this is function");
}
다음의 코드를 실행해보면,
undefined
undefined
this is function
이런 결과가 나온다. 이는 ret에 0이라는 값이 초기화되지 않은 것이다.
위 코드에서 호이스팅 결과와 동일한 코드를 만들어보면
var ret;
var sum;
function func(){
console.log("this is function");
}
console.log(ret)
console.log(sum)
func()
ret = 0;
이렇게 볼 수 있다. 즉, 선언과 동시에 초기화를 해준다고해도 선언만 쏙 빼놓고 호이스팅해준다는 것이다.
이로써 알 수 있는 것은 변수의 선언은 소스코드가 순차적으로 실행되는 시점인 런타임 이전에 먼저 실행되지만 값의 할당은 소스코드가 순차적으로 실행되는 시점인 런타임에 실행된다는 것이다
바로 이런 것이다.
1 단계인 자바스크립트 엔진이 선언하여 실행 컨텍스트에 등록하는 것은 '소스코드 평가 단계'로 런타임 이전이다. 이 때에는 변수에 undefined가 저장되어 있다.
2 단계인 자바스크립트 엔진에 의한 초기화 혹은 할당은 런타임에 작동하는 것이다.
var과 let과 같은 경우는 재할당이 가능하다.
let a = 10;
a =20; // 재할당
var b = 20;
b = 30; // 재할당
그러나 const 키워드가 쓰인 변수는 재할당이 불가능하다. 이를 상수라고 부르며, 상수는 절대 재할당이 불가능하다. 선언과 동시에 초기화를 해야 할당이 되는 것이다.
const a = 10;
a = 20; //불가
그런데, js에서 재할당이 일어나면 c/c++ 개발자에게는 아주 무서운 일이 벌어진다.
다음의 코드를 보자
var a = 10;
a = 20;
a = 30;
평범한 재할당 코드인 것 같지만 내부에는 이런 비밀이 숨겨져 있다.
먼저 10이라는 값이 변수(식별자) a에 의해 참조된다. a는 값으로 10이라는 값을 갖고 10이라는 값을 갖는 메모리 공간의 주소를 참조한다.
그런데, a = 20으로 재할당한다고 해보자.
우리의 머리 속은 a 메모리 공간에 있는 10 값을 20으로 바꾸면 된다고 생각하지만, js는 그딴 짓을 안해준다.
정말 순수한 의미의 재할당을 해준다. 또 메모리 공간을 하나 더 만드는 것이다. 즉, a에는 이제 새로운 메모리 주소 번지가 참조되는 것이다.
a =30을 해주면 똑같은 일이 반복된다.
또 새로운 메모리 공간을 할당하고 값을 채워주었다. a는 새로운 메모리 공간 주소를 참조하게 된다.
그렇다면, 10과 20이 있던 공간은 어떻게 되는가?? 이들은 자신을 참조하는 식별자가 없기 때문에 제거의 대상이 된다. 즉, 쓸모없는 녀석들이 된다는 것이다.
이처럼 제거의 대상이 되는 녀석들을 없애주는 녀석을 가비지 콜렉터라고 한다.
가비지 콜렉터는 메모리 공간을 주기적으로 검사하여 더 이상 사용되지 않는 메모리를 해제하는 기능을 한다. 단, 언제 해제되는 지는 정확히 알 수 없다. 여기서 사용되지 않는 메모리는 식별자로 참조되지 않는 메모리 공간을 의미한다.
이를 통해 우리는 메모리 누수를 막고, 프로그래밍을 쉽게 할 수 있는 것이다. c/c++ 개발자가 이 소리를 듣는 다면, 이건 사기야! 할 것이다. 임베디드 개발자 대부분은 메모리 누수로 개고생을 해본 적이 있을 테니까 말이다...
C언어와 같은언어는 개발자가 명시적으로 메모리를 할당하고 해제할 수 있기 때문에 언매니지드 언어라고 한다. 즉, 컴퓨터가 관리하지 않고 개발자에게 맡기는 언어라고 보면 된다.
반면, 자바나 자바스크립트와 같은 언어는 메모리의 할당과 해제를 위한 메모리 관리 기능을 개발자에게 직접적으로 제공하지 않는다. 메모리의 할당과 해제는 온전히 프로그래밍 언어의 내부 구현에 맡기며 이를 컴퓨터에 의해 관리를 받는다고 하여 매니지드 언어라고 한다. 개발자가 관여하지않아도 자동으로 해준다는 점은 정말 고맙지만, 이로 인한 성능 저하나 생산성이 안좋은 것은 사실이다.
비전공자로써 잘 모르던 부분인데 알기 쉽고 재밌게 설명해 주셔서 이해가 한번에 되었네요.
좋은 글 감사합니다.