스코프 체인

11hertz·2024년 3월 5일

YDKJSY

목록 보기
7/9
post-thumbnail

3.1 탐색의 진실

스코프와 중첩 스코프 사이에 맺어진 연결을 스코프 체인(scope chain)이라고 하며 변수 접근 시 사용할 경로가 스코프 체인을 통해 결정
체인은 변수 탐색 경로가 위 혹은 바깥으로만 향하도록 지시받음

탐색 과정 중 엔진은 현재 스코프를 관리하는 스코프 매니저에게 알고 있는 식별자 혹은 변수인지 물어보고, 원하는 식별자 혹은 변수를 찾을 때까지 중첩 스코프를 따라 올라가며 전역 스코프를 만날 때까지 탐색을 이어가다가 스코프 양동이에서 같은 이름을 가진 선언을 발견하면 즉시 탐색을 중단

런타임 검색 프로세스는 개념을 이해하는데 도움을 주지만 실제 탐색 프로세스와는 다름
구슬을 담은 양동이의 색(변수가 어떤 스코프에서 왔는지 알려주는 메타 정보)은 보통 컴파일 과정 초기에 결정되며 렉시컬 스코프는 컴파일 초기에 확정되므로 구슬 색은 런타임 상황에 영향을 받지 않음
구슬 색 역시 컴파일레이션 때 확정되고 변하지 않기 때문에 구슬 색에 관한 정보는 AST에 있는 각 변수(혹은 접근 가능한 변수) 정보와 함께 저장되며, 구슬 색 정보는 런타임을 구성하는 실행 가능한 명령에 의해 명시적으로 사용

프로그램이 실행되는 동안 JS 엔진은 변수가 속한 양동이 정보를 이미 알고 있기 때문에 변수가 어떤 양동이에서 왔는지 파악하기 위해 여러 스코프를 탐색할 필요가 없음
런타임에 탐색을 할 필요가 없다는 것은 최적화 관점에서 렉시컬 스코프가 가져다주는 중요한 헤택
변수 탐색에 시간을 쓰지 않아도 되기 때문에 런타임은 좀 더 효율적으로 작동할 수 있음

탐색 과정에서 선언을 찾지 못해도 항상 오류가 발생하는 것은 아님
런타임에 다른 파일(프로그램)이 해당 변수를 전역에 선언할 가능성도 있기 때문
접근 가능한 양동이에 원하는 변수가 선언되었는지 여부는 컴파일 타임이 아닌 런타임에 완전히 확정됨
선언하지 않은 변수에 대한 참조는 해당 파일을 컴파일하는 동안에는 색이 지정되지 않음
연관 파일들을 컴파일하고 애플리케이션 실행 직전까지 구슬 색이 지정되지 않음
탐색이 지연되는 경우는 변수가 발견되는 범위(주로 전역 스코프)를 기준으로 색이 정해짐

이러한 검색 프로세스는 변수당 최대 한 번만 일어나고, 변수의 스코프는 결정되면 변경되지 않기 때문에 런타임 중에는 그 어떤 것도 해당 구슬의 색을 변경할 수 없음

3.2 변수 섀도잉

렉시컬 스코프에 해당하는 양동이를 분류하는게 중요해지는 시점은 다음과 같음
두 개 이상의 변수가 있고, 각 변수들이 속한 스코프는 다르지만 이름은 같은 경우
이름이 같은 두 개 이상의 변수가 하나의 스코프에 있으면 안 되며 복수 참조가 발생하면 변수가 하나만 있는 것처럼 처리 됨
이러한 이유로 이름이 같은 변수를 두 개 이상 사용해야 한다면 스코프를 반드시 분리(중첩 스코프)해야 함

var student = 'Jimin';
function printStudent(studentName) {
	studentName = studentName.toUpperCase();
    console.log(studentName);
}
printStudent('Bora'); // BORA
printStudent(studentName); // JIMIN
console.log(studentName); // JIMIN

전역 스코프를 빨간 양동이, 함수 스코프를 파란 양동이라고 한다면 첫 번째 줄에 있는 변수 studentName은 빨간색 구슬, 세 번째 줄에 있는 함수 printStudent()의 매개변수 studentName은 파란색 구슬이 됨

할당문 studentName = studentName.toUpperCase()에 있는 두 studentName과 console.log(studentName)에 있는 studentName까지 세 개의 studentName 참조 모두 파란색 구슬이 됨
변수 탐색은 현재 스코프부터 시작해서 바깥과 위를 향해 스코프를 점차 넓혀가면서 진행되다가 일치하는 변수를 찾으면 중단 됨
printStudnet('Bora'); 에서 파란색 studentName을 찾으면 바로 탐색이 중단되는 이유이며 빨간색 구슬은 탐색 범위에 들어오지도 않음

이러한 렉시컬 스코프의 주요 특징을 섀도잉(shadowing)이라고 함
예시에서는 파란색 매개변수 studentName이 만든 그늘에 빨간색 변수 studentName이 가려짐
'매개변수가 전역 변수를 그늘로 가리고 있다'는 문장에서 그늘진(shadowed)라는 용어를 확실히 이해해야 함
함수에 인수를 넘겨 studentName을 재할당(re-assignment)해도 그 영향이 전역 변수이자 빨간색 구슬인 studentName에는 미치지 못하고 오직 내부 매개변수인 파란새 구슬 studentName에만 미치는 것은 섀도잉 때문

외부 스코프에 존재하는 변수는 같은 이름을 가진 변수를 선언해 변수를 섀도잉하면, 해당 스코프와 그 안쪽/아래쪽(중첩된 범위)에 있는 어떤 구슬도 섀도잉된 변수(예시의 경우 빨간색)와 같은 색으로 칠할 수 없음

3.2.1 전역 언섀도잉

전역 변수를 가린 변수가 있는 스코프에서 전역 변수를 접근할 수 있는 방법이 있음
전형적인 렉시컬 식별자 참조를 통해서가 아닌 다른 방법을 통하면 가능
전역 스코프에서는 var로 선언한 변수와 function 키워드로 선언한 함수는 전역 객체의 프로퍼티를 통해 접근할 수 있음
전역 객체는 본질적으로 전역 스코프를 객체로 나타낸 것으로 볼 수 있음
브라우저에서 실행되는 JS 코드를 작성해본 경험이 있다면 window가 전역 객체라고 알고 있겠지만 이 정보가 완전히 참은 아님

var studentName = '보라';
function printStudent(studentName) {
	console.log(studentName);
    console.log(window.studentName);
}
printStudent('지수');
// 지수
// 보라

window.studentName 참조 표현식은 전역 변수 studentName을 window(이번 섹션에서 전역 객체와 동일 취급)에 있는 프로퍼티처럼 접근하고 있음
그늘을 만든 변수가 있는 스코프에서 그늘로 가려진 변수에 접근할 수 있는 방법은 이렇게 전역 객체를 통하는 방법 뿐

window.studentName은 전역 변수 studentName을 거울에 비친 값이지 복제본이 아님
따라서 코드 한쪽에서 값을 변경하면 다른 쪽 코드에서도 변경 사항이 반영 됨
window.studentName을 실제 변수 studentName에 접근하게 해주는 게터(getter)나 세터(setter)라고 생각할 수 있지만, 사실은 전역 객체에 프로퍼티를 만들거나 값을 설정하면 전역 스코프에 변수 추가가 가능

가능하다고 해서 무조건 해야 하는 것은 아니며, 구현상 반드시 접근해야 할 전역 변수를 섀도잉하거나, 역으로 가려진 전역 변수를 꼼수를 사용하여 접근하지 말아야 함
전역 변수를 공식적인 방법으로 선언하는 대신 window에 프로퍼티를 추가하는 방식을 사용하면 코드 읽는 사람을 헷갈리게 할 수 있으므로 사용하지 말 것

해당 꼼수는 전역 스코프 변수에 접근할 때만 작동하고, var나 function으로 선언했을때만 작동
전역 스코프에서 다른 방법으로 선언한 것들은 전역 객체 프로퍼티에 거울 효과를 내지 못함
전역 스코프가 아닌 스코프에 존재하는 변수는 어떻게 선언했는지와는 상관없이 그림자에 가려진 스코프에서 접근할 수 없음

3.2.2 복사와 접근은 다릅니다

var special = 42;
function lookingFor(special) {
	var another = {
    	special: special
    };
    function keepLooking() {
    	var special = 3.141592;
        console.log(special);
        console.log(another.special); // 꼼수 사용
        console.log(window.special);
    }
    keepLooking();
}
lookingFor(112358132134);
// 3.141592
// 112358132134
// 42

special: special은 매개변수 special의 값을 또 다른 컨테이너(같은 이름을 가진 프로퍼티)에 복사해 전달
별개의 컨테이너에 값을 넣게 되면(another도 가려지지 않는 이상) 당연히 섀도잉이 효과를 발휘하지 못함
섀도잉이 효과가 없다는 말이 매개변수 special에 접근 가능하다는 말은 아님
매개변수 special에 접근할 때는 객체 프로퍼티라는 별개의 컨테이너를 통해 컨테이너에 복사된 값에 접근한다는 뜻이며 keepLooking() 안에서 매개변수 special에 값을 재할당할 수 없음
객체나 배열을 사용한다고 해도 참조 복사본을 통해 객체의 내용을 수정하는 것은 렉시컬 환경을 고려해 변수 자체에 접근하는 것이 아니기 때문에 객체 참조를 사용한다고 해도 여전히 special에 값을 재할당할 수 없음

3.2.3 금지된 섀도잉

모든 선언 조합이 섀도잉을 만들어내지는 않음
let은 var를 가릴 수 있지만, var는 let을 가릴 수 없음

function something() {
	var special = '자바스크립트';
    {
    	let special = 42; // 이 조합은 OK
        // ...
    }
}
function another() {
	// ...
    {
    	let special = '자바스크립트';
        {
        	var special = '자바스크립트';
            // ^^^ SyntaxError(구문 오류 발생)
            // ...
        }
    }
}

SyntaxError가 발생한 이유는 var가 같은 이름을 사용해 let으로 선언한 변수의 경계를 가로지르려고(뛰어넘으려고) 했기 때문이며, 이는 허용되지 않은 행위임

그러나 경계 뛰어넘기 금지는 함수 경계를 만났을 때는 효과를 발휘하지 못하기 때문에 다음 예시는 정상적으로 작동

function another() {
	// ...
    {
    	let special = '자바스크립트';
        ajax('https://some.url', function callback() {
        	//	허용된 섀도잉
            var special = '자바스크립트';
            // ...
        });
    }
}

내부 스코프에 있는 let은 외부 스코프에 있는 var를 언제나 가릴 수 있으며, 내부 스코프에 있는 var는 둘 사이에 함수 경계가 있는 경우에만 외부 스코프에 있는 let을 가릴 수 있음

3.3 함수 이름 스코프

function askQuestion() {
	// ...
}

함수 선언문은 함수를 둘러싸는 스코프에 식별자를 생성함

var askQuestion = function() {
	// ...
};

위의 예시에서도 마찬가지로 변수 askQuestion이 만들어지지만 함수 표현식(function expression)이기 때문에 함수 자체가 호이스팅 되지 않음

var askQuestion = function ofTheTeacher() {
	// ...
};

askQuestion은 외부 스코프에 그치지만 식별자 ofTheTeacher는 함수 안에 식별자 그 자체로 선언됨

var askQuestion = function ofTheTeacher() {
	console.log(ofTheTeacher);
};
askQuestion();
// function ofTheTeacher()...
console.log(ofTheTeacher);
// ReferenceError: ofTheTeacher is not defined

ofTheTeacher는 함수 밖이 아닌 안에 선언되는 것뿐만 아니라 읽기 전용으로 선언됨

var askQuestion = function ofTheTeacher() {
	'use strict';
    ofTheTeacher = 42; // TypeError
    // ...
};
askQuestion(); // TypeError

위의 예시에는 엄격 모드가 적용되었기 때문에 할당 실패 시 TypeError가 발생하며 비엄격 모드였다면 별도의 오류 없이 그냥 할당에 실패

var askQuestion = function () {
	// ...
};

이름 식별자가 있는 함수 표현식은 '기명 함수 표현식'이라 부르며, 위의 예시처럼 이름 식별자가 없는 경우에는 '익명 함수 표현식'이라 함
익명 함수 표현식은 스코프에 영향을 미치는 이름 식별자가 없음

3.4 화살표 함수

ES6에서 함수 표현식을 만들 수 있는 새로운 방법인 화살표 함수(arrow function)이 도입

var askQuestion = () => {
	// ...
};

화살표 함수를 사용하면 함수를 정의할 때 function 코드를 쓸 필요가 없음
매개변수 리스트를 감싸는 ()도 때에 따라서 생략 가능
함수 본문을 감싸는 {} 역시 경우에 따라서 생략 가능하며, {}를 생략한 경우에는 return 키워드 없이도 값을 반환

화살표 함수는 렉시컬 스코프 관점에서 익명 함수로 취급되며, 화살표 함수는 함수를 참조하는 연관 식별자와 직접 연결되어 있지 않음

아래 예시처럼 askQuestion에 할당을 하면 askQuestion라는 이름이 자체적으로 추론되긴 하지만 기명 함수와 똑같이 작동하는 것은 아님

var askQuestion = () => {
	// ...
};
askQuestion.name; // askQuestion

화살표 함수는 개발자에게 다양한 형태나 조건에 대한 유추를 비용으로 떠넘기면서 그 간결성을 유지

() => 42;
id => id.toUpperCase();
(id, name) => ({ id, name });
(...args) => {
	return args[args.length - 1];
};

화살표 함수는 익명이라는 특성, 그리고 명확한 형식이 없다는 특성 이외에는 function을 사용해 선언한 함수와 동일한 렉시컬 스코프 규칙을 적용받음
함수 본문을 감싸는 대괄호가 있든 없든 화살표 함수는 별도의 내부 중첩 스코프를 형성하는데, 이 중첩 스코프에 선언된 변수는 일반 함수의 본문 내에 선언한 변수 스코프와 동일하게 작동

3.5 정리

선언을 하든 표현식을 쓰든, 함수를 정의하면 새로운 스코프가 만들어지며, 스코프 중첩을 어떻게 만들었는지에 따라 스코프 계층 관계가 만들어지는데 이를 스코프 체인이라 부름
스코프 체인은 변수를 위 혹은 바깥쪽 방향으로만 접근 가능하도록 통제

새 스코프가 생기면 변수를 넣을 새로운 공간이 만들어지며, 스코프 체인 내 다른 계층에 있는 스코프에 이름이 같은 변수가 있으면 섀도잉 때문에 바깥 스코프에 있는 변수에 접근할 수 없게 됨

profile
Practice Makes Perfect!

0개의 댓글