조각조각 - 스코프(Scope)

eocode·2024년 3월 4일
1
post-thumbnail

목차

  1. 스코프
    1.1. 스코프?
    1.2. 전역 vs 지역 vs 블록 스코프
    1.3. 스코프 체인
    1.4. 실행 컨텍스트와 블록 레벨 스코프
    1.5. 정적 스코프 vs 동적 스코프
  2. 참고 자료

이전 실행 컨텍스트 게시글에서 어휘적 환경의 구성 요소인 외부 어휘적 한경 참조를 함수가 선언된 시점의 스코프이며 이 참조값을 통해 외부 스코프로 접근이 가능하다고 설명하였습니다. 이번엔 스코프란게 무엇인지 정리해보도록 하겠습니다.

1. 스코프

1.1 스코프(Scope)란?

스코프란 모든 프로그래밍 언어에서 사용되는 기본적이고 중요한 개념으로 변수나 함수 등의 식별자에 접근할 수 있는 유효 범위를 뜻합니다. 스코프를 통해 다르게 표현하자면 선언된 위치에 따라 식별자가 다른 코드에서 참조될 수 있을 지 없을 지가 결정됩니다.

자바스크립트 스코프는 전역 스코프, 지역 스코프(함수 스코프, 블록 스코프)가 존재합니다. 여기서 블록 스코프는 함수 스코프의 불편함을 해소하고자 es6에서 새롭게 추가되었습니다. 아래에서 각각의 스코프 특징을 자세히 알아보겠습니다.

자바스크립트는 정적 스코프를 따릅니다. 따라서 변수의 선언 위치에 따라 접근 가능 여부가 결정됩니다. 즉 선언문의 구조만을 살펴보면 스코프 체인을 파악할 수 있어 호출 마다 스코프가 변동되는 동적 스코프보다 변수 유효 범위 파악이 쉽습니다.

1.2 전역 vs 지역 vs 블록 vs 함수 스코프

scope

전역 스코프

코드의 가장 바깥 영역에서 선언된 변수나 함수는 전역 스코프를 갖습니다. 따라서 이 변수와 함수는 코드의 어느 위치에서든 참조될 수 있습니다.

위 그림을 살펴보면 constType 변수가 코드의 가장 바깥 영역에서 선언되어 전역 스코프를 가집니다. constType 변수는 func 함수 내부에서도 참조가 가능하며 함수 내부 if 블록에서도 참조가 가능합니다.

지역 스코프

코드의 가장 바깥 영역이 아닌 함수 또는 특정 블록 내부에 선언된 변수와 함수가 지역 스코프를 갖습니다. 이 변수와 함수는 자신이 선언된 스코프 내부에서만 참조가 가능합니다.

자바스크립트의 지역 스코프는 es6 이전까진 함수 스코프만 존재했으나 es6에서 블록 스코프 개념이 추가되었습니다. 블록 스코프가 추가되었어도 함수 스코프는 여전히 존재하고있습니다. 따라서 개발자가 의도를 가지고 두 스코프중 하나를 선택해서 사용할 수 있습니다.

변수가 선언된 타입에 따라 다른 스코프 규칙을 따릅니다. 이유는 간단합니다. es6 이전 var 타입 변수만 존재해왔고 이 var 타입 변수는 함수 스코프를 가집니다. es6 이후 추가된 let, const 타입 변수의 경우 var 타입 변수의 문제점들을 보완하고자 등장한 것으로 블록 스코프를 가집니다. 따라서 var, const, let 타입 변수 모두 혼용 가능한 현재의 자바스크립트의 경우 어느 변수 타입을 사용하느냐에 따라 스코프 규칙이 달라지게 됩니다.

함수 레벨 스코프 (var)

함수 내에서 변수나 함수가 var 타입 변수로 선언된 경우입니다. 이 경우 함수 스코프를 갖게되어 자신이 선언된 함수 내부에서만 접근이 가능합니다. 즉, 함수 외부에서 함수 내부에서 선언된 변수에 접근할 수 없습니다.

블록 레벨 스코프 (let, const)

코드 블록 내에서 변수나 함수가 let, const 타입 변수로 선언된 경우 입니다. 이 경우 블록 스코프를 갖게 되어 자신이 선언된 코드 블록과 그 하위 코드 블록에서 접근할 수 있습니다.자신이 선언된 코드 블록 외부에서는 접근할 수 없습니다.

1.3 스코프 체인과 스코프 체이닝

스코프 체인

위에서 전역, 함수, 블록 스코프를 살펴보았습니다. 스코프들은 서로 연결되어 계층 구조를 이루게 됩니다. 예를 들면 전역 스코프에 여러 함수 스코프 또는 블록 스코프가 포함 될 수 있고 또 함수 스코프 내부에도 또 다른 함수 스코프나 블록 스코프가 포함 될 수 있습니다. 이 스코프 계층 구조를 스코프 체인이라고 합니다.

스코프 체이닝

스코프 체이닝이란 간단히 말하자면 스코프 체인에서 식별자가 값을 찾아가는 과정입니다. 이 과정중에 스코프 계층 구조에 따라 즉 스코프 체인을 통해 내부 스코프에서 외부 스코프로 이동하며 값을 찾게 됩니다. 현재 스코프에서 변수를 찾을 수 없을때 외부 스코프로 이동하여 변수를 찾고 이 과정은 반복적으로 진행되며 결과적으로 최상위 스코프인 전역 스코프까지 이동해 변수를 탐색하게 됩니다.

🚨 외부 스코프로 이동하기 위해 함수 실행 컨텍스트에서 외부 어휘적 환경 참조 값을 참조해야합니다. 외부 어휘적 환경 참조 값은 함수 객체의 내부슬롯에서 받아 온 값인데 이 과정은 이 글에 자세히 정리되어 있습니다.(함수 객체 부분)

아래에서 코드와 함께 알아보겠습니다.

현재 스코프에서 변수 참조 가능한 경우

var x = "전역 변수";

function outer() {
  var x = "외부 함수 변수";
  function inner() {
    var x = "내부 함수 변수";
    console.log(x); // 내부 함수 변수
  }
  inner();
  console.log(x); // 외부 함수 변수
}

outer();
console.log(x); // 전역 변수

변수 x가 전역 코드, outer 함수, inner 함수에 선언됩니다. 따라서 전역 스코프 안에 outer 함수 스코프가 존재하고 outer 함수 스코프 안에 inner 함수 스코프를 가지는 계층 구조, 스코프 체인이 형성됩니다.

console.log로 x를 출력할 때마다 해당 스코프에 변수 x와 그 값이 존재합니다. 따라서 외부 스코프로 이동할 필요 없이 x의 값을 출력 할 수 있습니다. 외부 스코프로 이동하여 값을 찾아나가는 과정이 없었기 때문에 이 경우 스코프 체이닝이 진행되었다고 볼 수 없습니다.

출력 결과

내부 함수 변수
외부 함수 변수
전역 변수

현재 스코프에서 변수 참조 불가능한 경우

var x = "전역 변수";

function outer() {
  var x = "외부 함수 변수";
  function inner() {
    console.log(x); // ???
  }
  inner();
  console.log(x); // 외부 함수 변수
}

outer();
console.log(x); // 전역 변수

위 코드를 살짝 변경한 코드입니다. inner 함수 내부에 변수 x 선언문을 지워보았습니다.

outer 함수가 실행되면 inner 함수 내부 console.log가 가장 먼저 실행됩니다. 이때 inner 함수 스코프에 변수 x가 존재하지 않습니다. 따라서 자바스크립트 엔진은 외부 함수로 이동해 x를 찾게됩니다. 다행히도 변수 x가 outer 함수 내부에서 선언 되었기 때문에 값을 찾을 수 있어 '외부 함수 변수'가 출력됩니다.

다음으로 실행되는 console.log는 outer 함수 내부의 console.log입니다. 현재 스코프인 outer 함수 스코프에 변수 x가 존재하고 '외부 함수 변수'를 가지고 있으므로 '외부 함수 변수'를 출력합니다.

마지막으로 실행되는 console.log는 전역 코드에 존재하는 console.log입니다. 전역 스코프에 변수 x가 존재하고 '전역 변수' 값을 가지므로 '전역 변수'를 출력합니다.

정리하자면 첫번째로 실행된 console.log에서 변수 x를 찾을 때 스코프 체이닝이 발생하였고 이후 두번째, 세번째 console.log에선 단순히 해당 스코프의 변수를 찾아 출력하였습니다.

출력 결과

외부 함수 변수
외부 함수 변수
전역 변수

var x = "전역 변수";

function outer() {
  function inner() {
    console.log(x); // ???
  }
  inner();
  console.log(x); // ???
}

outer();
console.log(x); // 전역 변수

코드를 한번 더 수정해 보았습니다. 이번엔 outer 함수 내부에 존재했던 변수 x 선언문이 지워졌습니다.

위와 동일하게 진행됩니다. outer 함수가 실행되면 inner 함수 내부 console.log가 가장 먼저 실행됩니다. 해당 스코프인 inner 함수 스코프에 변수 x가 존재하지 않습니다. 따라서 외부 outer 함수로 이동하여 변수 탐색을 계속 진행합니다. 그런데 이번에는 아까와 다르게 outer 함수 스코프에도 변수 x가 존재하지 않습니다. 따라서 또 한번 외부 함수, 전역 코드로 이동합니다. 이제서야 전역 스코프에 "전역 변수" 값을 가진 변수 x가 찾아내어 "전역 변수"를 출력합니다. 즉, 스코프 체이닝으로 변수 x를 탐색하였습니다.

다음으로 실행되는 console.log는 outer 함수 내부의 console.log입니다. outer 함수 스코프에 변수 x가 존재하지 않아 외부 스코프, 전역 코드로 이동해 변수 탐색을 이어 나갑니다. 전역 스코프에서 "전역 변수" 값을 가진 변수 x를 찾아내고 값을 출력합니다. 마찬가지로 이번에도 스코프 체이닝으로 변수 x를 찾을 수 있었습니다.

마지막으로 실행되는 console.log는 전역 코드에 존재하는 console.log입니다. 전역 스코프에 변수 x가 존재하고 '전역 변수' 값을 가지므로 '전역 변수'를 출력합니다.

정리하자면 첫번째로 실행된 console.log와 두번째로 실행된 console.log에서 스코프 체이닝이 발생하였고 마지막 console.log에서만 단순히 해당 스코프의 변수를 찾아 출력하였습니다.

출력 결과

전역 변수
전역 변수
전역 변수

1.4 실행 컨텍스트와 블록 레벨 스코프 💡💡💡

이전 게시글에서 실행 컨텍스트를 알아보았습니다. 실행 컨테스트의 생성과 소멸 과정을 간단히 하면 아래와 같습니다.

  1. 코드 실행
  2. 전역 코드 평가 과정
  3. 전역 실행 컨텍스트 콜스택에 추가
  4. 함수 실행
  5. 함수 실행 컨텍스트 콜스택에 추가
  6. 함수 종료(함수 실행 컨텍스트 콜스택에서 제거)
  7. 전체 코드 종료(콜스택 내부 모든 요소 제거)

즉 함수가 실행되면 함수 실행 컨텍스트가 생성됩니다. 이 과정에서 어휘적 환경의 환경 레코드에 변수 및 함수 등 실행에 필요한 정보들이 저장됩니다. 이 환경 레코드 내부 변수와 함수는 함수 스코프를 따를까요? 블록 스코프를 따를까요?

함수 실행 컨텍스트의 생성으로 등록된 변수와 함수이기 때문에 함수 스코프를 가질것 같지만 답은 '식별자 타입에 따라 다르다' 입니다.

따라서 var 타입 변수가 선언되었다면 함수 스코프를 가지겠지만 let, const 타입 변수가 선언되었다면 블록 스코프를 가지게 됩니다. 그렇기 때문에 코드 블록 내부에 let, const 타입 변수가 선언되었다면 새로운 블록 레벨 스코프를 생성해야합니다. 이를 위해 새로운 어휘적 환경 생성하고 외부 어휘적 환경 참조 값이 블록이 시작되지 전 렉시컬 환경을 가르키게 해야합니다.

예제 코드로 알아보겠습니다.

const const_Global='global';

const func = () =>{
	var var_Func = 'func';
	console.log(var_Func);
	if(true){
		let let_Block = 'block';
		console.log(let_Block);
	}
}

func();

코드 실행 과정을 살펴 보겠습니다.

  1. 자바스크립트 엔진이 코드를 평가합니다.
  2. 전역 컨텍스트가 생성 과정이 진행됩니다.
  3. 전역 변수 const_Global이 선언되어 전역 실행 컨텍스트의 어휘적 환경에 담깁니다. (정확히는 const 타입 이기때문에 전역 실행 컨텍스트의 어휘적 환경 내부 중 선언적 환경 레코드에 담깁니다.)
  4. 자바스크립트 엔진 평가 단계가 모두 완료되어 실행 단계가 진행됩니다.
  5. func 함수가 호출됩니다.
  6. 자바스크립트 엔진이 func 함수를 평가 합니다.
  7. func 함수 실행 컨텍스트 생성 과정이 진행됩니다.
  8. 함수 내부 var 타입 변수 var_Func가 선언되어 func 함수 어휘적 환경의 환경 레코드에 저장됩니다.
  9. 평가 단계 진행 중 블록 내부 let 타입 변수 선언이 있음을 감지합니다.
  10. 블록 어휘적 환경을 생성합니다. 환경 레코드에 let_Block 변수가 담기며 외부 어휘적 환경 참조 값이 블록 밖 어휘적 환경을 가르킵니다.
  11. 함수 func를 평가 단계가 모두 진행되어 이어서 실행 단계가 진행됩니다.
  12. var_Func이 출력됩니다.
  13. if 블록에 진입합니다.
  14. let_Block이 출력됩니다.

여기서 12, 14 과정의 var_Func 변수와 let_Block 변수는 각각 어떤 스코프를 가지고 이때 실행 컨텍스트는 어떤 어휘적 환경을 가르킬까요?

func 함수 내부 if 블록 진입 전후

callstack2

위 그림과 같이 블록 레벨 스코프에 진입하기 전엔 func 함수 실행 컨텍스트가 func 함수 어휘적 환경을 가르킵니다. 따라서 'func'가 출력됩니다.

callstack3

이후 if 블록에 진입하면 func 함수 실행 컨텍스트가 블록 어휘적 환경을 가르킵니다. 그렇기 때문에 'block'이 출력됩니다. 만약 if 블록 내부에서 변수 var_Func를 출력한다면 블록 어휘적 환경의 외부 어휘적 환경 참조 값을 이용해 스코프를 외부로 이동해 'func'를 출력할 수 있습니다.

callstack2

if 블록을 빠져나오면 func 함수 실행 컨텍스트는 다시 func 함수 어휘적 환경을 가르킵니다.

1.5 정적 스코프 vs 동적 스코프

위에서 언급하였듯이 자바스크립트는 정적 스코프를 따릅니다. 정적 스코프와 대조되는 개념은 동적 스코프 입니다. 동적 스코프의 특징도 알아보며 두 개념을 비교해 보도록 하겠습니다.

스코프의 유효 범위

스코프는 변수의 유효 범위를 결정합니다. 따라서 변수의 스코프가 정적 스코프를 따르는지 동적 스코프를 따르는지에 따라 다른 유효 범위를 가지게 됩니다.

정적 스코프 유효 범위

정적 스코프는 컴파일 시간에 스코프가 결정되는 방식으로 대부분의 프로그래밍 언어는 정적 스코프를 사용합니다. 컴파일 시간에 스코프가 결정된다는 것은 쓰여진 선언 코드를 보고 스코프가 정해진다는 것입니다. 즉 변수의 선언 위치에 따라 전급 가능 여부가 결정됩니다. 이 덕분에 함수 호출 등 추가적인 과정을 살펴볼 필요 없이 코드 구조만 살펴보면 스코프를 파악할 수 있어 가독성과 예측성이 높습니다.

var x = "전역 변수";

function a() {
  console.log(x);
}

function b() {
  var x = "지역 변수";
  a();
}

b(); // global

코드의 과정을 살펴 보겠습니다.

  1. '전역 변수'값을 가진 변수 x가 전역 코드에서 선언됩니다. 이 변수는 전역 스코프를 따릅니다.
  2. 함수 b가 호출 됩니다.
  3. 함수 b 내부에서 '지역 변수'값을 가진 변수 x가 선언됩니다. 이 변수는 함수 b 스코프를 따릅니다.
  4. 함수b 내부에서 함수 a가 호출 됩니다.
  5. 함수 a 내부에서 x를 탐색하고 출력합니다.

여기서 x는 어떤 값을 가질까요??

우선 함수 a 스코프에는 변수 x가 존재하지 않습니다. 따라서 외부 스코프로 이동하여 변수 x를 찾아나가게 됩니다. 정적 스코프와 동적 스코프를 나누는 기준은 바로 이 외부 스코프로 나가는 과정에 있습니다.

  • 경우 1 : 함수 a가 선언된 코드에서 외부로 나가면 전역 스코프로 나가게 됩니다.
  • 경우 2 : 함수 a가 호출된 코드에서 외부로 나가면 함수 b 스코프로 나가게 됩니다.

여기서 정적 스코프는 바로 '경우 1'에 해당합니다. 따라서 함수 a가 선언된 코드에서 외부로 이동하여 변수 탐색을 계속 진행하게 됩니다. 함수 a가 선언된 코드이 외부는 전역 스코프이므로 전역 스코프의 변수 x의 값인 'global'이 출력됩니다.

동적 스코프 유효 범위

동적 스코프는 실행 시간에 스코프가 결정되는 방식으로 많은 곳에서 사용되지는 않습니다. 실행 시간에 스코프가 결정된다는 것은 함수가 언제, 어디에서 호출되는지 상황에 따라 다른 스코프를 가지고 변수가 결정된다는 뜻입니다. 따라서 단순 코드 구조만으로 스코프를 파악할 수 없고 프로그램의 실행 흐름을 따라야 스코프를 파악할 수 있습니다. 그렇기 때문에 가독성과 예측성이 낮다는 단점이 존재합니다.

var x = "전역 변수";

function a() {
  console.log(x);
}

function b() {
  var x = "지역 변수";
  a();
}

b(); // local

이 코드는 위 정적 스코프 설명에서 사용된 코드와 동일합니다. 동일한 코드지만 동적 스코프를 따른다는 가정하에 코드를 살펴보겠습니다.

코드 실행 과정은 위와 동일하게 진행됩니다.

  1. '전역 변수'값을 가진 변수 x가 전역 코드에서 선언됩니다. 이 변수는 전역 스코프를 따릅니다.
  2. 함수 b가 호출 됩니다.
  3. 함수 b 내부에서 '지역 변수'값을 가진 변수 x가 선언됩니다. 이 변수는 함수 b 스코프를 따릅니다.
  4. 함수b 내부에서 함수 a가 호출 됩니다.
  5. 함수 a 내부에서 x를 탐색하고 출력합니다.

이번엔 변수 x는 어떤 값을 가질까요??

  • 경우 1 : 함수 a가 선언된 코드에서 외부로 나가면 전역 스코프로 나가게 됩니다.
  • 경우 2 : 함수 a가 호출된 코드에서 외부로 나가면 함수 b 스코프로 나가게 됩니다.

동적 스코프를 따르기 때문에 '경우 2'에 해당합니다. 함수 a가 호출된 코드에서 외부 스코프로 이동하고 변수를 탐색합니다. 즉 외부 스코프인 함수 b 스코프로 이동해 변수 x를 탐색하고 'local'을 찾아 'local'을 출력하게 됩니다.

결과적으로 함수 a 안에 console.log 코드가 무엇을 출력하는지 알기 위해 함수 a가 어디에서 호출되는지도 추가적으로 파악해야 했습니다. 코드가 복잡한 경우 동적 스코프가 사용되면 스코프 파악이 매우 어려울것이라고 생각됩니다... 자바스크립트는 정적 스코프를 따라서 다행이네요 ㅎㅎ..

2. 참고 자료

profile
프론트엔드 개발자

0개의 댓글