[JS]클로져(closure)와 클로져의 사용 예제

Kyle·2020년 12월 23일
27

javascript

목록 보기
8/18

클로저란?

MDN에서는 closure를 이와 같이 정의한다.

"A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time."

클로저는 주변의 상태 (lexical environment)의 참조와 함께 번들로 묶인 함수의 조합입니다. 즉, 클로져는 우리에게 inner함수에서 outer함수의 스코프에 접근을 가능하게 해줍니다. 자바스크립트에서 클로저는 함수가 생성될 때마다 생성됩니다.

간단히 말하자면 함수가 선언될 때(실행X) 외부의 lexcial environment를 참조?하게 되는 현상?이다.

정의만 봤을 때는 이게 뭔가 싶다
클로져를 정확하게 이해하기 위해서는 실행컨텍스트에 대해 알아야 한다.
실행컨텍스트와 lexical environment에 정확히 안다면 클로져를 모를 때 궁금증이 생길것이다. 이게 된다고?

클로져 간단한 예제

함수의 실행컨텍스트를 간단히 알고 이해해야한다.
함수는 호출 될 때 함수의 실행컨텍스트가 생성됐다가 실행이 끝나면 실행컨텍스트가 종료된다. (힘수의 실행 컨텍스트가 생성될 때 함수의 lexical environment도 생성된다.)
이 실행컨텍스트의 lexical environment에는 함수의 지역변수의 정보& 이 함수의 상위 스코프의 대한 정보가 들어있다.

function outer(){
  const name = 'kyle';
  console.log(name)
}
sayName() //kyle
console.log(name) //error

위의 코드에 대한 결과는 어떻게 보면 당연하다. 여기서 우리가 변수 name을 함수 밖에서 사용하고 싶다면 어떻게 사용해야할까??
변수 nameouter이라는 함수의 실행컨텍스트가 종료 되면서 아무도 접근할 수 없게됐다. name을 함수 밖에서도 사용하기 위해서는 클로져를 사용하면 된다.

function outer(){
  const name = 'kyle';
  console.log(name)
  return function inner(){
    const greeting = 'hello!'
    console.log(greeting,name)
  }
}
const getKyle = outer() //kyle 
getKyle() //hello!kyle

outer 함수가 위와 같이 종료됐다.
우리의 예상대로라면 음~ outer함수가 종료 됐으니 name은 아무데서도 접근할 수 없다!
하지만 inner함수에서 접근 가능하다.

왜?? 왜 가능하죠??
이것이 바로 클로져다.

클로져의 특성상 inner함수가 선언될 때 그 주변의 lexical enviroment(여기서는 outer의 lexical enviroment)와 함께 번들로 묶였기 때문이다!

그렇기 때문에 inner가 실행이 되어서 lexical environment를 만든 뒤 참조 하지 않아도, 선언할 때 이미 묶여버리게 된다.

->때문에 변수 name을 사용할 수 있게 된다.

그럼 여기서 또 궁금해진다.

원래 원리를 깨면서까지 왜 클로저라는 것을 만들었나??

클로저가 필요한 이유

1. 전역변수를 줄일 수 있다.

전역변수가 많으면 어디에서든 실수로라도 접근을 할 수 있기 때문에 최대한 전역변수를 줄여서 코드를 해야합니다.

하지만 프로그램을 구현하다보면 이 함수 하나에서만 사용하는데 전역변수가 필요한 순간이 있죠. 이럴 때 클로저가 유용하게 사용됩니다.

예를들어 클릭할 때 마다 count를 세주는 함수가 있다고 생각해봅시다.

const btn = document.querySelector('button')

btn.addEventListener('click',handleClick)

let count = 0
function handleCilck(){
  count++
  return count
}

위와 같은 경우에 count를 전역변수로 사용해줘야 count가 증가를 해줄 수 있습니다.
이럴경우 클로져를 사용해서 해결할 수 있습니다.

const btn = document.querySelector('button')

btn.addEventListener('click',handleClick())

function handleCilck(){
  let count = 0
  return function (){
    count++
    return count
  }
}

위와 같이 작성해 준다면 외부함수(handleClick)의 lexical environment를 참조하는 함수를 btn의 콜백함수로 이용해 전역객체 없이 구현할 수 있습니다.

2. 비슷한 형태의 코드를 재사용률을 높일 수 있습니다.

말 만으로는 무슨 뜻인지 이해가 잘 안갈 수 있습니다.
아래 코드를 보면서 이해해보겠습니다.

const newTag = function(open, close) {
    return function(content) {
        return open + content + close
    }
}

const bold = newTag('<b>', '</b>')
const italic = newTag('<i>', '</i>')

console.log(bold(italic("This is my content!")))
//<b><i>This is my content!</i></b>

코드를 보면 bold,itealic 등등의 새로운 태그를 만들 수 있는 함수 newTag를 클로져를 이용해 간단하게 구현했습니다.

인자에 open,close,content를 한번에 다 받는다면,This is my content! 와 같은 값을 출력을 하고 싶을 때 가독성이 떨어질 수 있습니다.

하지만 클로져로 구현하면 코드의 가독성도 좋은 재사용하기 편한 코드를 구현할 수 있습니다.

for문과 var을 쓸 때는 조심해야한다.

let,const를 사용하는 지금에는 발생하지 않는 문제이지만 클로저를 활용할 수 있는 예제이기 때문에 갖고 왔다. 여기서 문제점은 var는 블럭스코프가 아니기 때문에 for문 밖에서도 접근이 가능하다.


let funcArr =[]

for(var i = 0; i < 5; i++) {
 var c = i *2;
 funcArr.push( (_) => console.log(c))
}
funcArr.forEach( fn => fn() )

위의 함수를 예상해보자 어떻게 출력할까?
8, 8, 8, 8, 8
이라고 출력이 나온다. funcArr에 저장된 함수들이 각각의 var c를 저장하고 있지 않기 때문이다. 그래서 for문이 완료된 var c= 8을 출력하게 된다.

이문제는 클로저를 활용해서 해결할 수 있다.

let funcArr =[]

for(var i = 0; i <5; i++) {
 ( (_) => {
   var c = i * 2;
   funcArr.push( (_) => console.log(c) )
 })()
}

funcArr.forEach( f => f() )

즉시실행함수로 클로져로 만들면서 외부 함수의 lexical environment(여기에서 c=i*2) 참조하고 있기 때문에 우리가 예상한대로 출력하게 된다.
0, 2, 4, 6, 8

착각하기 쉬운 클로저

아래의 코드가 클로저인지 생각해보자

function outer() {
  let name = 'kyle';
  if (true) {
    let city = 'seoul';
    return function inner() {
      console.log(city);
    };
  }
}

클로저를 정확하게 파악하지 않았을 때 위와 같은 코드를 클로저라고 착각할 수 있다. 마치 함수를 리턴하는 것 자체가 클로저라고 오해하는 경우가 생길 수 있다.


하지만 위의 코드의 스코프를 보면 그렇지 않다는 것을 알 수 있다.

그렇다. 클로저는 내부에 선언된 함수가 외부함수의 지역변수를 사용해 줬을 때만 클로저라고 선언된다.

inner 함수에도 클로저를 사용하고 싶으면 name변수를 사용해주면 된다.

function outer() {
  let name = 'kyle';
  if (true) {
    let city = 'seoul';
    return function inner() {
      console.log(city);
     console.log(name);
    };
  }
}

클로저란 내부함수에서 외부함수의 지역변수를 사용할 때 외부함수의 lexcial environment와 함께 bundled 되는 것?(mdn에서는 combination이라고 표현했다.) 이라고 생각하면 될 것이다.

급 마무리..?

위의 예시 외에도 setTimeout함수 라던지 매우 다양한 곳에서 클로져는 다양하게 활용될 수 있다. 클로저를 공부하면서 이해가 안되는것들을 공부하면서 정말 많은 것을 배울 수 있었다.
물론 아직도 이해가 안되는 것들이 더 많다. 그것들을 하나씩 학습하면서 포스팅하면 언젠가는 완벽하게 이해하는 날이 오지 않을 까 생각한다.

참조: https://blog.bitsrc.io/closures-in-javascript-why-do-we-need-them-2097f5317daf

profile
Kyle 발전기

4개의 댓글

comment-user-thumbnail
2021년 10월 6일

잘읽었습니다

답글 달기
comment-user-thumbnail
2021년 12월 28일

감사합니다!!

답글 달기
comment-user-thumbnail
2022년 1월 3일

착각하기 쉬운 파트 부분은 굉장이 유용하고 중요하네요! 배워갑니다!

답글 달기
comment-user-thumbnail
2023년 3월 5일

감사합니다

답글 달기