클로저는 일반적으로 어떤 함수가 자신의 내부가 아닌 외부에서 선언된 번수에 접근하는 것을 뜻한다.
다음과 같이 미국 달러를 대한민국 원으로 환전해주는 간단한 함수를 예를 들어 살펴보자. 이 함수는 미국달러를 인자로 받아 함수 내부에 선언된 환율을 이용하여 대한민국 원으로 환전환 결과를 반환한다.
function convertUsToKrw(dollar){
const rate = 1113.5;
return dollar * rate;
}
인자로 5달러를 넘겨서 이 함수를 호출하면 예상대로 5567.5원이 반환된다.
> convertUsToKrw(5)
5567.5
이번엔 환율(rate)을 함수 외부에 선언하면 어떨까?
const rate = 1113.5;
function convertUsToKrw(dollar){
return dollar * rate;
}
이 함수를 동일한 인자를 넘겨 호출해보면 완전히 동일한 결과값이 반횐되는 것을 알 수 있다.
> convertUsToKrw(5)
5567.5
이렇게 JS에서 함수는 매개변수와 로컬변수 뿐만아니라 외부에서 선언된 변수도 자유로 자유롭게 접근을 할 수 있다. 그리고 이렇게 함수가 자신의 밖에서 선언된 변수에 접근하는 것을 클로저라고 한다.
JS를 사용하며너서 개발자들은 알게 모르게 이미 작성하는 코드의 많은 부분에서 클로저를 사용하고 있다. 특히 어떤 함수 내에서 또 다른 함수를 선언할 때 알게 모르게 클로저를 자주 사용하게 된다.
function batchConvertUsdToKrw(dollars) {
const rate = 1113.5;
const convertUsdToKrw = (dollar) => dollar * rate;
return dollars.map(convertUsdToKrw);
}
> batchConvertUsdToKrw([1, 2, 10, 20, 50, 100])
[ 1113.5, 2227, 11135, 22270, 55675, 111350 ]
JS 배열의 map()메서드의 인자로 convertUsToKrw()함수가 넘어가고 있다. 여기서 batchConvertUsToKrw()함수의 내부에서 선언된 rate변수는 convertUsdToKrw() 함수의 입장에서 보면 외부에서 선언이 되어 있다. 즉, convertUsdToKrw()함수는 자신의 내부가 아닌 외부에서 선언된 rate변수에 접근하고 있으므로 정확히 위에서 정의한 클로저라는 것을 알 수 있다.
다른 예로 회원가입을 위한 signUp()함수를 작성한다고 가정해보자. 이 함수는 내부적으로 사용자 생성과 알람 전송을 위해 각각 createUser()와 sendNotifications()함수가 정의되어 있고 호출되고 있다.
function signUp(username, password, email, phone){
const createUser = () => {
console.log(`${username}과 ${password}를 검증 중...`)
console.log('사용자 생성중...')
// DB에 사종자 레코드 저장하는 코드
}
const sendNotifications = () => {
console.log(`${email}로 이메일 전송 중...`)
console.log(`${phone}로 문자 전송 중...`)
// 실제로 알람을 전송하는 코드
}
createUser();
sendNotifications();
console.log('메인 페이지로 이동...')
}
signUp()함수는 4개의 매개 변수를 받고 있는데 createUse()함수와 sendNotification()함수 입장에서 보면 모두 외부에서 선언된 변수들이다. createUser()함수는 외부에서 선언된 username, password 변수에 접근하고 있고, sendNotifications()함수는 외부에 선언된 email과 phone에 접근하고 있다. 다시말해 signUp() 함수 내부에는 2개의 클로저가 있는 것이다.
여기서 재미 있는 클로저의 특징을 찾아 볼 수 있는데 자세히 보면, createUser()함수와 sendNotification()함수에 어떤 매개 변수도 필요가 없고, signUp()함수 내에서 호출할 때 어떤 인자도 넘길 필요가 없다. 만약에 이 두개의 함수를 signUp()함수 외부로 빼낸다면 어떻게 될까?
const createUser = (username, password) => {
console.log(`${username}과 ${password}를 검증 중...`)
console.log('사용자 생성중...')
// DB에 사종자 레코드 저장하는 코드
}
const sendNotifications = (email, phone) => {
console.log(`${email}로 이메일 전송 중...`)
console.log(`${phone}로 문자 전송 중...`)
// 실제로 알람을 전송하는 코드
}
function signUp(username, password, email, phone){
createUser(username, password);
sendNotifications(email, phone);
console.log('메인 페이지로 이동...')
}
위와 같이 createUser()함수와 sendNotification()함수에는 매개 변수가 필요하게 되고 signUp()함수 내에서 호출할 인자를 넘겨줘야 한다. 이와 같이 클로저를 활용하면 어떤 함수 내부에서만 사용되는 일회성 함수(정의 후 바로 호출되는는)의 매개 변수를 생략할 수 있다.
위에서 살펴본 클로저의 특징은 과용하거나 오용하게 되면 오히려 코드 품질 측면에서 부정적인 영향을 미칠 수가 있다. 왜냐하면 클로저가 많아지게 되면 코드가 읽거나 고치기가 어려워지고 버그가 발생해기 쉬워지기 때문이다.
예를 들어 이전에 작성한 batchConvertUsdToKrw()함수 내부에 선언되어 있던 rate 변수를 밖으로 let 키워드를 사용하여 빼내 보자.
let rate = 1113.5;
function batchConvertUsdToKrw(dollars) {
const convertUsdToKrw = (dollar) => dollar * rate;
return dollars.map(convertUsdToKrw)
}
이렇게 되면 rate변수로 인해서 batchConvertUsdToKrw() 함수에게도 클로저가 생기고 convertUsdToKrw()함수 입장에서는 중첩 클로저가 생기게 된다. 이 함수가 짧으니까 다행이지, 매우 긴 함수였다면 rate변수의 출처가 어디인지 알아내려면, 두 겹의 함수 네임 스페이스를 뒤지느라 곤혹스러웠을 것이다.
뿐만아니라 let키워드를 사용해서 rate변수를 선언했기 때문에 rate변수에 할당된 값을 batchConvertUsdToKrw()함수 외부에서 자유롭게 바꿀 수가 있다.
> batchConvertUsdToKrw([1, 5])
[ 1113.5, 5567.5 ]
> rate = 100
100
> batchConvertUsdToKrw([1, 5])
[ 100, 500 ]
이러한 문제는 심각한 버그로 이어질 수 있어서 값이 바뀔 수 있는 외부 변수에 접근할 때는 각별히 주의해야한다.
특히, 이러한 버그는 비동기 처리시에 발생할 확률이 더욱 높아진다. 아래 프로그램을 실행해보면 rate 변수 중간에 할당된 값을 무시되고 제일 마지막에 할당한 값이 계속해서 출력되는 것을 볼 수 있는데 setTimeout() 함수 때문에 콘솔에 출력되는 시점이 1초씩 지연되기 때문에 발생하는 현상이다.
let rate = 1113.5;
function printRate() {
setTimeout(() => console.log(`현재 미달러 환율은 ${rate}원 입니다.`), 1000);
}
printRate(); // 현재 미달러 환율은 500원 입니다.
rate = 100;
printRate(); // 현재 미달러 환율은 500원 입니다.
rate = 500;
printRate(); // 현재 미달러 환율은 500원 입니다.
중첩 클로저를 피하는 간단한 방법은convertUsdToKrw()함수를 batchConvertUsdToKrw()함수 밖으로 빼주어 최대한 클로저가 중첩되지 않도록 해주는 것이다.
let rate = 1113.5;
const convertUsdToKrw = (dollar) => dollar * rate;
function batchConvertUsdToKrw(dollars) {
return dollars.map(convertUsdToKrw);
}
이렇게 내부에서 정의된 함수를 외부로 빼면 이 함수에 대해서도 단위 테스트를 작성할 수 있으며 이 함수는 batchConverUsdToKrw()함수를 벗어나 다른 곳에서도 호출이 가능해진다.
한발 더 나아가 아예 rate를 batchConvertUsdTokrw()함수의 변수로 넣어주면 rate변수의 값이 외부에서 수정될 수 있는 문제를 근본적으로 예방할 수 있다.
function batchConvertUsdToKrw(dollars, rate) {
const convertUsdToKrw = (dollar) => dollar * rate;
return dollars.map(convertUsdToKrw);
}
이제는 batchConvertUsdToKrw()함수를 호출할 때 항상 명시적으로 rate인자를 넘져줘야 하기 때문이다.
> batchConvertUsdToKrw([1, 5], 1113.5)
[ 1113.5, 5567.5 ]
> batchConvertUsdToKrw([1, 5], 100)
[ 100, 500 ]
1. 데이터를 보존할 수 있다.
클로저 함수는 외부 함수의 실행이 끝나더라도 외부함수 내 변수를 사용할 수 있다.
클로저는 이처럼 특정 데이터를 스코프 안에 가두어 둔 채로 계속 사용할 수 있게 하는 폐쇄성을 갖는다.
2. 정보의 접근 제한(캡슐화)
'클로져 모듈 패턴'을 사용해 객체에 담아 여러개의 함수를 리턴하도록 만든다.
이러한 정보의 접근을 제한하는 것을 캡슐화라고 한다.
3. 모듈화에 유리하다.
클로저 함수를 각각의 변수에 할당하면 각자 독립적으로 값을 사용하고 보존할 수 있다.
이와 같이 함수의 재사용성을 극대화한 함수 하나를 독립적인 부품의 형태로 분리하는 것을 모듈화라고 한다.
클로저를 통해 데이터와 메소드를 묶어 다닐 수 있기데 클롣저는 모듈화에 유리하다.