Scope와 Closure(클로저)

Yun Hyuk Ko·2021년 2월 12일
0

Scope & Closure

목록 보기
1/1

Scope

scope란? 한글로는 "범위"라는 뜻을 가지고 있다.

프로그래밍을 할 때, 우리는 범위를 고려해야한다. 왜냐하면, 어디까지가 잘 작동하는 코드이고, 어디까지가 잘 작동되지 않는지 파악할 수 있다.

즉, 잘 작동하는 코드를 작성하는데 있어서 반드시 알아야 할 기초이다.

javascript에 있어서 scope의 의미는 무엇인지 확인해보자. 그리고 프로그래밍에 있어서 왜 중요한지 보자.

먼저 다음 예시 코드를 가지고 문제를 통해 확인해보자.

Q : greetSomeone()과 fistName 실행의 결과는?

let greeting = "Hello";

function greetSomeone(){
  let fistName = "Josh";
  return greeting + " " + firstName;
}

greetSomeone(); // "Hello Josh"
firstName; // Reference Error

greetSomeone()의 결과는 "Hello Josh"이다.
firstName의 결과는 레퍼런스 에러가 뜨게된다.

밖에 적혀있는firstNamegreetSomeone()안에 접근을 할 수가 없다.
즉, 우리 눈에는 보이지 않는 무언가 바운더리가 있음을 알 수 있다.

function greetSomeone(){ // local scope
  let fistName = "Josh";
  return greeting + " " + firstName;
}

변수 firstName에 접근 할 수 있는 범위가 존재한다.
function greetSomeone(){...}는 local scope로 local scope 안쪽에서 선언된 변수는 밖에서 사용 할 수 없다.

[그림으로 확인해 보자.]

자, 이제 scope라는 것이 무엇인지 정의를 하자면, 아래와 같다.

  • Scope: 변수 접근 규칙에 따른 유효 범위
  • 변수는 어떠한 환경 내에서만 사용 가능하며, 프로그래밍 언어는 각각의 변수 접근 규칙을 가지고 있다.
  • 변수와 그 값이, 어디서부터 어디까지 유효한지를 판단하는 범위
  • Javascript는 기본적으로, 함수가 선언되는 동시에 자신만의 Scope를 가진다.

Rule 1 : Local Scope vs Global Scope

Local Scope와 Global Scope의 차이점을 보자.

  • 안쪽 Scope에서 바깥 변수/함수를 접근하는 것은 가능
  • 바깥쪽 Scope에서 안쪽 변수/함수를 접근하는 것은 불가능

이러한 규칙들로 우리가 파악해 볼 수 있는 것은 아래와 같다.

  • Scope는 중첩이 가능하다. - 함수 안에 함수를 넣을 수 있다.
  • Global Scope는 최상단의 Scope로, 전역변수는 어디서든 접근이 가능하다.
  • 지역(Local) 변수는 함수 내에서 전역(Global) 변수보다 더 높은 우선순위를 가진다.

마지막으로 예제를 보면서 (let)선언이 없을 경우의 차이점을 확인해보자.

case 1 :let 선언이 있는 경우

let name = "Richard";

function showName() {
  let name = "Jack"; // 지역 변수
  // showName 함수 안에서만 접근 가능
  console.log(name); // ???
}

// 코드에서 순서대로 콘솔에 출력되는 결과는?
console.log(name); // Richard
showName(); // Jack
console.log(name); // Richard

case 2 : let 선언이 없는 경우

let name = "Richard";

function showName() {
  name = "Jack"; // 전역 변수
  // 선언(let)이 없기 때문에, 바깥 scope에 있는 name이라는 변수를 가져옵니다
  console.log(name); // ???
}

// 코드에서 순서대로 콘솔에 출력되는 결과는?
console.log(name); // Richard
showName(); // Jack
console.log(name); // Jack

Rule 2 : Function Scope vs Block Scope

  • Block : 중괄호로 시작하고, 끝나는 단워
if(true){ // 중괄호 시작
  console.log("i am in the block")
} // 끝
for(let i=0; i<10; i++){ // 중괄호 시작
  console.log(i);
} // 끝
{ // 중괄호 시작
  console.log("it works")
} // 끝

보통의 경우는 scope를 구분 할 수 있는 단위로서 생각 할 수 있는데, 그럼 function scope와 Block scope의 차이점이 무엇인지 다음 문제를 통해 확인해 보자.

  • Q : 콘솔에 출력되는 결과는?
for(let i=0; i<5; i++){
  console.log(i); // 다섯 번 interation
}

console.log("final i: ", i); // reference Error
// block 범위를 벗어나는 즉시 변수를 사용할 수 없다

reference Erorr가 뜨는 것은 정의된 i를 찾을 수 없다는 것인데, 즉, i는 특정한 block 내에 범위가 한정되어 있다라는 것을 알 수 있다.

reference Erorr가 뜨는 이유는 위의 주석처럼 block 범위를 벗어나는 즉시 변수를 사용할 수 없기 때문이다.

그럼 let키워드 대신 var키워드를 이용해 변수를 선언해서 다시보자.

  • Q : 콘솔에 출력되는 결과는?
// function scope
for(var i=0; i<5; i++){ // block
  console.log(i); // 다섯 번 interation
}

console.log("final i: ", i); // 5
// 범위를 벗어나도(같은 function scope에서는) 시용이 가능하다.

여기서 i는 block 안쪽에서만 사용 할 수 있는 것이 아니라, 하나의 function scope 안쪽에서 사용이 가능하다.

위 코드의 function 구분이 없는 관계로 코드 자체로 하나의 function scope를 가지고 있다.

여기서 우리는 let, const, var 이 세가지 차이점의 대해 알아둘 필요가 있다.

let	// 유효범위: block / 값 재정의: 가능 / 재선언: 불가능
const	// 유효범위: block / 값 재정의: 불가능 / 재선언: 불가능
var	// 유효범위: function / 값 재정의: 가능 / 재선언: 가능

여기서, 개발자 콘솔로 var 키워드와 let 키워드를 확인해보자.

  • 변수를 정의하는 또다른 키워드var의 작동원리를 통해 function scope와 Block scope가 무엇인지 알아보자.

var를 통한 변수 선언

function greetSomeone(firstName){
  var time = "night";
  if(time === "night"){
    var greeting = "Good night";
  }
  
  return greeting + " " + firstName;
}

greetSomeone("steve"); // "Good night steve"

let을 통한 변수 선언

function greetSomeone(firstName){
  let time = "night";
  if(time === "night"){ // 이 Block 안의 변수 greeting은 참조 할 수 없다.
    let greeting = "Good night";
  } // 여 block 안의 greeting 변수는 이 안쪽에서만 사용이 가능하다.
  
  return greeting + " " + firstName;
}

greetSomeone("steve"); // Reference Error
let을 통해 변수를 선언했을 때 아래와 같이 콘솔 결과를 확인 할 수 있다.

  • Reference Error로 greeting 변수를 찾을수 없다고 나온다.
  • greeting 변수는, 해당 Block 안쪽에서만 접근이 가능하다.

Closure(클로저)

closure은 "scope의 연장이다"라고 알고있으면 된다. 문제를 하나보면서 알아보자.

  • Q : 다음 코드에서 innerFn 함수에 접근 할 수 있는 Scope는 총 몇개일까?
function outerFn(){
  let outerVar = "outer";
  console.log(outerVar);
  
  function innerFn(){
    let innerVar = "inner";
    console.log(innerVar);
  }
}

let globalVar = "global";
outerFn();

innerFn 함수에 접근 할 수 있는 Scope는 3개다. 아래의 그림을 통해 확인해 보자.

  • 함수도 return을 할 수 있다.
function outerFn(){
  let outerVar = "outer";
  console.log(outerVar);
  
  function innerFn(){
    let innerVar = "inner";
    console.log(innerVar);
  }
  return innerFn;
}

outerFn(); // outer
// console 창에 outerVar의 값이 찍히고,
// 아직 실행되지 않은 함수 innerFn가 return되는 것을 확인 할 수 있다.

다음의 경우 각각 콘솔에 어떻게 찍힐까?

outerFn()(); // "outer" "inner"
let innerFn = outerFn(); // "outer"
innerFn(); // "inner"
  • 클로저(closure) : 외부 함수의 변수에 접근 할 수 있는 내부 함수
    이 패턴은 자주 쓰이고 유용하다. 잘 알아두자.

유용한 클로저(Closure) 예제

  • 커링 : 함수 하나가 n개의 인자를 받는 대신, n개의 함수를 만들어 각각 인자를 받게 하는 방법
function adder(x){
  return function(y){
    return x + y;
  }
}

adder(2)(3); // 5

let add100 = adder(100); // x의 값을 고정해 놓고 재사용 가능
add100(13) // 113
add100(10) // 110

let add5 = adder(5);
add5(2) // 7
const adder = x => y => x + y;

adder(2)(3); // 5

let add100 = adder(100); // x의 값을 고정해 놓고 재사용 가능
add100(13) // 113
add100(10) // 110

let add5 = adder(5);
add5(2) // 7
  • 외부 함수의 변수가 저장되어 마치 템플릿 함수와 같이 사용 가능
function htmlMaker(tag){
  let startTag = "<" + tag + ">";
  let endTag = "</" + tag + ">";
  
  return function(content){
    return startTag + content + endTag;
  }
}

let divMaker = htmlMaker("div");
divMaker("code"); // <div>code</div>
divMaker("states"); // <div>states</div>

let h1Maker = htmlMaker("h1");
h1Maker("HeadLine"); // <h1>HeadLine</h1>
  • 클로저(Closure) 모듈 패턴 : 변수를 Scope 안쪽에 가두어 함수 밖으로 노출시키지 않는 방법
function makeCounter(){
  let privateCounter = 0;
  
  return {
    increment: function() {
      privateCounter++;
    },
    decrement: function() {
      privateCounter--;
    },
    getValue: function() {
      return privateCounter;
    }
  }
}

let counter1 = makeCounter();
counter1.increment();
counter1.increment();
counter1.getValue(); // 2

let counter2 = makeCounter();
counter2.increment();
counter2.decrement();
counter2.increment();
counter2.getValue(); // 1

// 두 Counter에 각기 다른 privateCounter를 다루면서,
// privateCounter를 밖으로 노출시키지 않는다.
// 위의 return 값을 변수 obj에 담아서도 할 수 있다.
function makeCounter(){
  let privateCounter = 0;
  
  let obj = { // obj라는 변수에 담아서 return 해줄수도 있다.
    increment: function() {
      privateCounter++;
    },
    decrement: function() {
      privateCounter--;
    },
    getValue: function() {
      return privateCounter;
    }
  }
  return obj; // 변수 obj return
}

let counter1 = makeCounter();
counter1.increment();
counter1.increment();
counter1.getValue(); // 2

let counter2 = makeCounter();
counter2.increment();
counter2.decrement();
counter2.increment();
counter2.getValue(); // 1

비슷하지만 다른 에제도 있다. 보고 참고만해서 알고있자.

function makePayment(){
  let type = "현금"; // 반드시 "현금" 또는 "카드"여야 함
  
  return {
    payWithCash: function(amount){
      type: "현금";
      console.log(type + "으로" + amount + "만큼 지불합니다.");
    },
    payWithCard: function(amount){
      type: "카드";
       console.log(type + "으로" + amount + "만큼 지불합니다.");
    }
  }
}
// 위의 return 값을 변수 obj에 담아서도 할 수 있다.
function makePayment(){
  let type = "현금"; // 반드시 "현금" 또는 "카드"여야 함
  
  let obj = { // obj라는 변수에 담아서 return 해줄수도 있다.
    payWithCash: function(amount){
      type: "현금";
      console.log(type + "으로" + amount + "만큼 지불합니다.");
    },
    payWithCard: function(amount){
      type: "카드";
       console.log(type + "으로" + amount + "만큼 지불합니다.");
    }
  }
  return obj; // 변수 obj return
}
profile
기억보다 좋은건 기록이다

0개의 댓글