JavaScript | 스코프와 클로저

1. 개념


1.1 스코프의 의미와 적용 범위

컴퓨터 공학에서 스코프는 변수의 유효범위를 뜻한다. 범위는 블록 또는 함수를 기준으로 나뉘어진다. 변수가 블록 안쪽에 선언 되었는가, 바깥쪽에 선언되었는가에 따라 변수의 유효 범위가 달라진다.

1.2 스코프의 주요 규칙들

  • 바깥쪽 스코프에서 선언한 변수는 안쪽 스코프에서 사용 가능하다. 하지만 안쪽에서 선언한 변수는 바깥쪽 스코프에서는 사용 불가능하다.

  • 스코프는 중첩이 가능하다. 겹겹이 쳐진 울타리를 상상하면 된다. 가장 바깥쪽의 스코프를 전역 스코프(global scope)라고 부르고, 다른 스코프는 전부 지역 스코프(local scope)이다.

    • 전역 스코프에서 선언한 변수는 전역 변수, 지역 스코프에 선언한 변수는 지역 변수라고 한다.
    • 스코프 체인
  • 지역 변수는 전역 변수보다 우선순위가 높다.

    • 변수 이름이 같을 경우 전역 변수가 지역 변수에 의해 가려지는 현상을 쉐도잉(variable shadowing)이라고 부른다.
let name = 'suri'; // 전역 변수

function showName() {
    let name = 'masuri'; // let 키워드로 지역 변수 선언
    console.log(name);
}

console.log(name); // suri
// 안쪽 스코프에 있는 지역 변수에 접근할 수 없다.
showName(); // masuri
// 안쪽 스코프에 새로 선언한 지역 변수 name은 전역 변수 name과 다르고, 변수명이 동일하지만 높은 우선순위를 가진다.
console.log(name); // suri

==========비교==========

let name = 'suri';

function showName() {
    name = 'masuri'; // let 키워드를 사용하지 않았다. 전역변수 name을 그대로 사용한다. 같은 변수다.
    console.log(name);
}

console.log(name); // suri
showName();  // masuri
// 함수가 실행되고 난 이후에는 전역변수 name의 값이 바뀌므로 바뀐 값이 출력된다.
console.log(name); // masuri  
  • 아래 코드 살을 붙이면, 함수 내에서 선언 키워드 없는 선언은 함수의 실행 전까지 선언되지 않은 것으로 취급한다.

1.3 스코프의 종류

블록 스코프 block scope

중괄호를 기준으로 범위를 구분한다.

함수 스코프 function scope

function 키워드로 함수를 선언하면 함수 스코프가 만들어진다. 함수의 실행부터 종료까지다. 단, 화살표 함수를 사용하면 블록 스코프로 취급한다.

function showName(){
	console.log('suri');
} // 표현식

let showName = function() {
	console.log('suri') 
} // 선언식

1.4 let, const, var

var

  • 블록 스코프를 무시하고 함수 스코프만 따른다.
  • 화살표 함수의 블록 스코프는 무시하지 않는다.
  • var 선언은 함수 스코프 최상단에 선언된다. 선원 키워드가 없는 선언은 최고 스코프에 선언된다.
for (let i = 0; i < 3; i++) {
console.log('hello');
}
console.log(i); // ReferenceError

=======비교=======

for (var i = 0; i < 3; i++) {
console.log('hello');
}
console.log(i); // 3
  • 블록 스코프 안에서 정의된 변수는 그 범위를 벗어나면 접근할 수 없다. ReferenceError를 던진다. e.g. for문 조건식에 들어가는 변수 i를 블록 스코프 밖에서 출력하려고 하는 경우

  • var 키워드로 선언된 변수 i는 블록 스코프를 벗어나도 같은 함수 스코프에서는 사용이 가능하다. 즉, 블록 스코프를 따르지 않고 함수 스코프를 따른다.

var 키워드보다 let 키워드를 권장하는 이유

  • var는 블록 스코프를 무시하므로, 스코프에 대한 이해가 없으면 혼란스러울 수 있다. 따라서 var 보다는 let으로 변수 선언 하기를 권장한다.

  • let 키워드는 재선언을 방지한다. 코딩할 때 변수 재선언을 할 필요는 없다. 보통 이런 경우는 버그이기 때문이다. let 키워드로 실수를 방지할 수 있다.

const

  • 블록 스코프를 따른다.
  • 값을 새롭게 할당할 필요가 없으면 const 키워드 사용이 권장된다.
  • 값을 재할당 할 경우 TypeError를 낸다. 실수로 값이 변경되는 것을 막을 수 있다.

1.5 전역 객체(window)

브라우저에는 window 객체가 존재하다. (콘솔 창 확인) 브라우저 창을 의미하는 객체이기도 하고, 전역 항목을 담고 있기도 하다. var로 선언된 전역 변수와 전역 함수가 window 객체에 속한다.

var myAge = '22';
console.log(window.myAge); // 22

function foo() {
  console.log('you are young');
} // 선언식으로 함수를 선언

var fooo = function() {
  console.log('hello');
} // var 키워드로 표현식으로 함수를 선언

console.log(foo === window.foo); // true
console.log(fooo === window.fooo); // true
             

1.6 변수 선언 tip

  • 전역 변수는 최소화하기
    • 전역변수는 가장 바깥 스코프에서 정의한, 어디서든 접근 가능한 변수이다.
    • 스코프를 신경쓰지 않아도 된다는 생각에 너도나도 남발하면 부작용이 발생할 수 있다.
  • let과 const를 주로 사용하기
    • var로 선언한 전역변수가 window 기능을 덮어쓸 수도 있다.
  • 선언 키워드를 사용해 변수를 할당하기
    • 선원 키워드가 없는 변수는 var로 선언된 전역 변수처럼 작동한다.
  • strict mode를 적용하기
    • js 파일 상단에 'use strict'를 쓴다.
    • 이 경우 위의 선언 키워드 없는 변수를 에러로 잡아낸다.

1.7 개발자 도구 breakpoint

Sources - 파일 열기 - 라인 클릭으로 breakpoint 지정 - 새로고침

  • Scope 탭에서 변수의 스코프와 값을 확인해볼 수 있다.
  • breakpoint를 이용해 내가 원하는 라인에서 단계별로 코드를 실행해 볼 수 있겠다.

1.8 클로저 정의

여러 참고서에서 클로저에 대한 다양한 정의들이 있다.

  • 함수와 함수가 선언된 어휘적 환경의 조합이다.
  • 자신을 내포하는 함수의 컨텍스트(외부 함수의 스코프)에 접근할 수 있는 함수다.
  • 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수다.
  • 이미 생명 주기 상 끝난 외부함수의 변수를 참조하는 함수
  • 어떤 함수에서 선언한 변수를 참조하는 내부 함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상.

=> 함수 내에서 다른 함수(내부함수)가 리턴이 되면, 이 함수를 클로저 함수라고 부르고, 내부 함수가 외부 함수에 있는 변수에 접근이 가능하다. 1차로 이렇게 정리한다.

이거 읽어보기....

클로져는 "함수와 함수가 선언된 어휘적(lexical) 환경의 조합을 말합니다. 이 환경은 클로저가 생성 된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다." 라고 합니다.

여기서의 키워드는 "함수가 선언"된 "어휘적(lexical) 환경"입니다. 특이하게도 자바스크립트는 함수가 호출되는 환경와 별개로, 기존에 선언되어 있던 환경 - 어휘적 환경 - 을 기준으로 변수를 조회하려고 합니다. 유어클레스 영상에서 언급되는 "외부함수의 변수에 접근할 수 있는 내부함수"를 클로져 함수로 부르는 이유도 그렇습니다.

클로저 함수: 클로저는 외부함수의 컨텍스트에 접근할 수 있는 내부함수를 뜻합니다. 외부함수의 실행이 종료된 후에도, 클로저 함수는 외부함수의 스코프, 즉, 함수가 선언된 어휘적 환경에 접근할 수 있습니다.

클로저 사용 예시: 클로저를 통해 커링(currying, 함수 하나가 n개의 인자를 받는 대신 n개의 함수를 만들어 각각 인자를 받게 하는 방법), 클로저 모듈(변수를 외부 함수 스코프 안쪽에 감추어, 변수가 함수 밖에서 노출되는 것을 막는 방법) 등의 패턴을 구현할 수 있습니다.

클로저의 단점: 일반 함수였다면 함수 실행 종료 후 가비지 컬렉션(참고 자료: MDN '자바스크립트의 메모리 관리') 대상이 되었을 객체가, 클로저 패턴에서는 메모리 상에 남아 있게 됩니다. 외부 함수 스코프가 내부함수에 의해 언제든지 참조될 수 있기 때문입니다. 따라서 클로저를 남발할 경우 퍼포먼스 저하가 발생할 수도 있습니다.

자바스크립트는 가비지 컬렉션을 통해 메모리 관리를 합니다. 객체가 참조 대상이 아닐 때, 가비지 컬렉션에 의해 자동으로 메모리 할당이 해제됩니다.

1.9 클로저 함수의 특징

const adder = x => y => x + y;
adder(5)(7); 
typeof adder(5) // 함수
adder(5) // y => x + y; 인데 여기에서 x가 5이다.

======비교======
const adder = function (x) {
  return function (y) { // 리턴값이 함수의 형태다.
    return x + y;
  }
}

======비교======
  let add = function(x) {
  let sum = function(y) {
    return x + y;
  }
  return sum;
}

let foo = add(1); // foo는 x에 1이 할당된 상태를 기억하고 있는 내부 함수 sum을 의미하는 것이다.
foo(3);
let total = foo(6); // total은 7의 값을 갖는다.
 
  1. 함수를 리턴하는 함수다.

    • 리턴값이 함수의 형태다.
    • 함수를 리턴하는 함수가 클로저 형태를 만든다.
  2. 내부 함수는 외부 함수의 변수에 접근 가능하다.

    • 리턴하는 함수에 의해 스코프가 구분된다.
    • 클로저는 스코프를 이용해 변수의 접근 범위를 닫는다.

1.10 데이터를 보존하는 클로저

외부 함수의 실행이 끝나도 외부 함수 내 변수가 메모리 상에 저장이 된다. (어휘적 환경을 저장하기 때문이다.)

const adder = function (x) {
  return function (y) {
    return x + y;
  }
}

const add5 = adder(5);
add5(7) // 12
add5(10) // 15

  • 변수 add5에는 함수가 담겨 있다.
  • 5가 할당된 변수 x를 메모리에 저장한 채로 남아 있다.

1.11 클로저를 이용한 패턴

클로저는 특정 데이터를 스코프 안에 가두어 둔 채로 계속 사용할 수 있게 해준다.

HTML 문자열 만들어보기

const tagMaker = tag => content => `<${tag}>${content}</${tag}>`

const divMaker = tagMaker('div');
divMaker('hello'); // '<div>hello</div>'
divMaker('suri');

const spanMaker = tagMaker('span');
spanMaker('hello'); // '<span>hello</span>'
spanMaker('som');

클로저 모듈 패턴

const makeCounter = () => {
    let value = 0;

    return {
        increase: () => {
            value = value + 1
        },
        decrease: () => {
            value = value - 1
        },
        getValue: () => value
    }
}

const counter1 = makeCounter();

  • 클로저를 이용해 내부 함수를 하나만 리턴하는 것이 아니고, 객체에 담아 여러 개의 내부 함수를 리턴할 수도 있다.
  • counter1은 함수 여러개를 포함한 객체다.
  • value 변수에 값을 새롭게 할당할 수 있나? NO. 외부 스코프에서 내부 스코프 변수에 접근 불가능하다는 규칙에 따라 불가능하다. 정보의 접근 제한(캡슐화)
counter1.increase();
counter1.increase();
counter1.increase();
counter1.getValue(); // 3

counter2.decrease();
counter2.decrease();
counter2.getValue(); // -2
  • counter1과 counter2 객체는 value 값을 독립적으로 가지게 된다. 서로에게 영향을 끼치지 않고 각각의 값을 보존할 수 있다.
  • 함수 재사용성을 높이고, 함수 하나를 독립적인 부품 형태로 분리하는 것을 모듈화라고 한다.

2. 에러로그


바깥 스코프에서 안쪽 스코프의 변수에 접근할 수 없다는 스코프 규칙에 따라 ReferenceError

===1===
let username = 'kimcoding';
if (username) {
  let message = `Hello, ${username}!`;
  console.log(message); // 'hello, kimcoding'
}
console.log(message); // reference 에러

===2===
  
  let greeting = 'Hello';
function greetSomeone() {
  let firstName = 'Josh';
  return greeting + ' ' + firstName;
}

console.log(greetSomeone()); // Hello Josh
console.log(firstName); // ReferenceError

클로저 함수가 재사용 될 때, 전역변수와 지역변수의 값이 어떻게 저장되는지? (cp 5)

  • console.log(++a, ++b)를 찍을 때, 값에도 실제 증감 연산자가 반영이 되는 걸 알 수 있다.

지역/전역 스코프 나누기 > 선언된 변수 찾고 지역/전역 변수인지 명시 > 선언되지 않은 변수가 가리키는 대상 안에서 밖으로 찾기

함수의 매개변수는 그 안에 새롭게 선언된 지역변수라고 개념적으로 이해한다.

3. 질문


  • 단, 화살표 함수를 사용하면 블록 스코프로 취급한다. 그래서 이게 뭐? var 키워드로 선언된 변수는 화살표 함수도 무시하지 않는다며..

  • const 키워드로 배열을 할당하고 pop push 등 메서드를 사용하는 거. 이건 재할당이라고 보진 않나보다.

  • 궁금한 게 if 조건문 안에 단순히 변수만 들어가 있으면 그 변수가 선언되었나?로 해석하면 되나요?

  • 스코프 8번 문제. inner(); 이게 무슨 의미일까. 함수를 호출해서 실행한다. 그냥 리턴값이 되는건가. 변수에 담지 않으면 활용은 하지 못하는건가. 함수 바디 안에 내용을 실행을 하고 (눈에 보이진 않지만) 리턴 값을 가지고 있다.

좋은 웹페이지 즐겨찾기