렉시컬 스코프

11hertz·2024년 3월 4일

YDKJSY

목록 보기
6/9
post-thumbnail

2.1 구슬과 양동이

스코프를 효과적으로 이해하기 위해 여러 색이 있는 구슬을 같은 색의 양동이에 분류하는 작업에 비유해 볼 수 있음
구슬을 같은 색끼리 모으고 빨간색 구슬은 빨간색 양동이에, 파란색 구슬은 파란색 양도이에, 초록색 구슬은 초록색 양동이에 넣는다고 가정하면 초록색 구술이 필요할 때 초록색 양동이로 가면 원하는 것을 얻을 수 있다는 것을 예상할 수 있음

구슬은 프로그램 내 변수를 의미하고, 양동이는 스코프(함수 혹은 블록)을 의미하며 양동이에 칠한 색은 각 스코프를 의미
변수의 스코프는 변수가 어디에서 선언되었는지에 따라 달라지기 때문에 각 구슬의 색은 원래 구슬이 어디에서 생성되었는지에 따라 결정

// 1) 외부 전역 스코프: 빨간색 버블
var students = [
	{ id: 14, name: '카일' },
    { id: 73, name: '보라' },
    { id: 112, name: '지수' },
    { id: 6, name: '호진' },
];
function getStudentName(studentID) {
	//	2) 함수 스코프: 파란색 버블
    for(let student of students) {
   		// 3) 반복문 스코프: 초록색 버블
        if(student.id = studentID) {
        	return student.name;
        }
    }
}
var nextStudent = getStudentName(73);
console.log(nextStudent); // 보라

1) 가장 바깥의 전역 스코프
전역 스코프를 감싸며 students, getStudentName, nextStudent라는 식별자/변수가 세 개 있음

2) 함수 getStudentName()의 스코프
함수 getStudnetName을 감싸며 매개변수 studentID라는 식별자/변수가 있음

3) for 반복문의 스코프
for 반복문을 감싸며 student라는 식별자/변수가 있음

스코프 버블은 함수/블록 스코프가 어디에 있느냐에 따라 컴파일 중에 결정
3)이 2)에 2)가 1)에 속하는 것처럼 스코프 블록은 서로 중첩이 가능
스코프 버블은 부모 스코프 버블에 온전히 포함되기 때문에 한 스코프가 두 개의 바깥 스코프에 동시에 포함되는 일은 절대 일어나지 않음

각 구슬(변수나 식별자)은 어떤 버블(양동이)에서 정의되었느냐에 따라 색이 결정되며, 어디서 접근 가능한지에 따라 결정되지 않음
JS 엔진은 컴파일 중에 프로그램을 처리하기 때문에 변수가 선언된 곳을 찾는다는 것은 "내가 지금 속한 스코프(버블 또는 양동이)가 무슨 색이야?" 라고 질문하는 것과 같음
질문에 대한 답을 받으면 변수는 자신이 속한 양동이/버블 색과 같은 색이 됨

스코프는 프로그램의 필요에 따라 원하는 대로 중첩해 사용할 수 있음
이미 선언된 변수나 식별자를 참조(비선언) 할 때는 현재 스코프나 현재 스코프의 위, 혹은 바깥 스코프에 참조하려는 변수나 식별자의 정의가 있는 경우 가능하지만, 정의가 아래 혹은 안쪽 스코프에 있으면 불가능

변수는 특정 스코프에서 선언되며 변수는 다양한 색의 구슬이고, 구슬은 같은 색 양동이에 담김

선언이 이루어진 스코프와 동일한 스코프에 있는 변수 참조 혹은 선언이 이루어진 스코프보다 더 깊은 스코프에 있는 변수 참조는 해당 스코프와 동일한 색을 가진 구슬이 됨
중간에 있는 스코프가 선언을 섀도잉(shadowing)하면 이러한 규칙이 적용되지 않음

양동이 색과 양동이에 어떤 구슬이 담길지는 컴파일 중에 결정
컴파일레이션 과정에서 확정된 정보는 프로그램 실행 중에 변수(구슬 색)를 탐색하는데 사용

2.2 JS 엔진 구성원 간의 대화

JS 엔진 구성원들의 역할은 다음과 같음

  • 엔진
    컴파일레이션을 시작부터 끝까지 책임지고 JS로 만든 프로그램을 실행함

  • 컴파일러
    파싱과 코드 생성 과정에서 일어나는 모든 잡일을 담당

  • 스코프 매니저
    선언된 모든 변수와 식별자를 담은 탐색용 목록을 작성하고 유지보수 함
    코드 실행 시, 선언된 변수와 식별자 접근 관련 규칙을 강제

var students = [
	{ id: 14, name: '카일' },
    { id: 73, name: '보라' },
    { id: 112, name: '지수' },
    { id: 6, name: '호진' },
];
function getStudentName(studentID) {
    for(let student of students) {
        if(student.id = studentID) {
        	return student.name;
        }
    }
}
var nextStudent = getStudentName(73);
console.log(nextStudent); // 보라

var를 통한 선언(var students = [ ... ])을 살펴보면
변수를 선언하고, 배열 리터럴을 해당 변수에 할당하는 코드를 봤을 때 우리는 일반적으로 이를 하나의 문으로 생각
하지만 실제로 JS는 이를 컴파일 중에 컴파일러가 처리하는 작업과 실행 중에 엔진이 처리하는 작업, 두 개의 별도 작업으로 나눠 해당 문을 처리

컴파일러가 var students = [ ... ]를 처리할 때 거치는 단계는 다음과 같음

1) 컴파일러는 var students를 만나면 스코프 매니저에 특정 스코프 양동이에 students라는 이름을 가진 변수가 있냐고 물어보고, 스코프 매니저가 그렇다고 대답하면 컴파일러는 선언을 무시하고 지나감
그렇지 않다는 대답을 들으면 컴파일러는 프로그램이 실행될 때 스코프 매니저에게 해당 스코프 양동이에 students라는 이름의 변수를 생성해 달라고 요청

2) 컴파일러는 프로그램 실행 시점에 엔진이 실행할 students = [] 할당문에 대한 코드를 생성
엔진은 추후 실행 시점에 컴파일러가 생성한 코드를 보고 스코프 매니저에게 현재 스코프 양동이에 students라는 이름을 가진 변수가 있냐고 물어봄
없다는 대답을 들으면 엔진은 상위 스코프로 타고 올라가면 studnets 변수를 찾음
students를 찾으면 엔진은 여기에 배열([ ... ])의 참조를 할당

위에 작성한 과정을 요약하면
1)의 과정에서 컴파일러는 스코프 변수 선언을 준비함(현재 스코프에 스코프 변수가 선언되어 있지 않은 경우)

2)의 과정에서 엔진은 엔진이 실행되는 동안, 문에서 할당 부분을 처리하기 위해 스코프 매니저에게 변수를 찾아달라고 부탁하고 변수를 undefined로 초기화해 사용할 준비를 함
그리고 그 이후에 변수를 배열에 할당

2.3 중첩 스코프

getStudentName()용 함수 스코프는 전역 스코프 안에 중첩되어 있음
for 반복문을 위한 블록 스코프 역시 함수 스코프 안에 중첩되어 있음
스코프는 프로그램을 어떻게 짜는지에 따라 중첩 깊이가 달라짐

모든 스코프는 한 번이든 여러 번이든 실행될 때마다 스코프에 해당하는 스코프 매니저 인스턴스를 갖게 되며 스코프가 실행될 때마다 자동으로 스코프 내 모든 식별자가 스코프에 등록됨
이를 변수 호이스팅이라고 함

스코프 시작 부분에서 식별자가 function 선언과 함께 등장했다면 해당 변수는 연관된 함수 참조로 자동 초기화
식별자가 let, const가 아닌 var 선언과 함께 등장한 경우, 해당 변수는 자동으로 undefined로 초기화되어 바로 사용 가능한 상태가 됨
var로 선언하지 않은 변수는 초기화되지 않은 상태(uninitialized)가 되어 엔진에 의해 선언 및 초기화가 완전히 끝날 떄까지 사용할 수 없음

렉시컬 스코프의 중요한 특징 중 하나는 현재 스코프에서 식별자 참조를 찾을 수 없을 때 해당 스코프를 감싸는 바깥 스코프에서 식별자 참조를 찾는다는 것
이런 프로세스는 원하는 식별자 참조를 찾거나 더 이상 찾을 만한 스코프가 없을 때까지 계속됨

2.3.1 탐색이 실패할 경우

엔진이 탐색 범위를 확장해가며 모든 렉시컬 스코프를 뒤졌는데도 원하는 식별자를 찾지 못한 경우에는 오류 발생 상태가 조성
프로그램의 모드(엄격 모드인지 아닌지)와 변수의 역할(타깃인지 소스인지)에 따라 오류가 다르게 처리 됨

  • undefined
    변수가 소스 역할을 할 때 식별자를 찾지 못하면 해당 변수는 선언되지 않은 undeclared(알 수 없는 혹은 누락된) 변수로 간주되어 ReferenceError가 발생
    변수가 타깃 역할을 하고 프로그램이 엄격 모드에서 실행되고 있을 때 역시 해당 변수는 선언되지 않은 변수로 간주되어 소스 역할 변수를 못 찾을때와 유사하게 ReferenceError 발생

대부분의 JS 실행 환경에서 선언되지 않은 변수 때문에 발생하는 오류 메세지는 "ReferenceError: XYZ is not defined." 와 유사함
오류 메세지의 not defined는 '정의되지 않은' 이라는 뜻을 가진 undefined와 거의 동일하지만 JS에서 not defineddhk undefined는 전혀 다른 단어임

접근 가능한 렉시컬 스코프에 식별자에 해당하는 변수 선언(declaration)이 있는 경우, not defined를 not declared 혹은 undeclared와 치환해서 생각해도 됨
하지만 undefined는 변수는 발견되었는데(선언은 되어 있는데) 해당 시점에 값이 없는 경우를 의미

JS에서 typeof 연산자는 선언되지 않은 변수나 값이 없는 변수를 넘겼을 때 똑같이 undefined를 반환하기 때문에 혼란이 가중될 수 있음
undefined라는 용어를 만나면 undefined인지 undeclared인지를 혼동하지 않게 세심한 주의를 기울여야 함

  • 전역 변수의 갑작스러운 등장
    변수가 타깃 역할을 하고 엄격 모드가 아닐 때에는 혼란과 예상치 않은 결과를 불러 일으키는 과거 잔재가 드러나기 시작함
    타깃 할당이라는 목적을 달성하기 위해 전역 스코프의 스코프 매니저가 돌발적으로 전역 변수를 만들어 버리는데 이러한 갑작스러운 동작이 문제를 일으킬 수 있음
function getStudentName() {
	// 변수가 선언되지 않았는데 할당을 함
    nextStudent = '보라';
}
getStudentName();
console.log(nextStudent);
// 뜬금없이 전역 변수가 생기면서 '보라'가 출력

선언된 적이 없는 변수에 값을 할당하는 것은 오류이므로 당연히 ReferenceError를 발생 시켜야 하지만 엄격 모드에서만 ReferenceError를 발생시키고 엄격 모드가 아닌 경우는 동작하지 않음
우발적으로 등장하는 전역 변수에 절대 의존하지 말아야 하며, 항상 엄격 모드에서 작업하고 변수는 반드시 선언해서 사용해야 함
이렇게 사용하면 선언되지 않은 변수에 값을 할당하려는 실수를 할 때 ReferenceError가 나타나기 때문에 오류의 원인을 정확하게 파악할 수 있게 됨

2.3.2 스코프 건물

중첩 스코프 검색 과정은 건물에 비유할 수 있음
건물은 프로그램의 중첩 스코프 모음이며 1층은 현재 실행 중인 스코프를 나타내고, 꼭대기층은 전역 스코프를 나타냄

타깃 혹은 소스 역할을 하는 변수 참조를 찾을 때는 지금 있는 층을 먼저 둘러봄
현재 층에서 원하는 변수 참조를 찾지 못하면 엘리베이터를 타고 다음 층(바깥 스코프)으로 가서 변수 참조를 찾음
계속해서 원하는 변수 참조를 찾다가 꼭대기층(전역 스코프)에 도달하면 원하는 변수 참조를 찾든 찾지 못했든 상관없이 탐색이 중단됨

profile
Practice Makes Perfect!

0개의 댓글