클로저(Closure)

자바스크립트에서 어떤 함수와 그 함수가 참조할 수 있는 값들로 이루어진 환경을 하나로 묶은 것을 의미한다. 다르게 표현하면 함수와 그 함수가 선언된 어휘적 환경의 조합이다. 다음 예시를 살펴보자.

출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures

가만 보면 뭔가 이상~하다. MakeFunc함수가 실행되면서 지역변수 name을 만들었다. 그리고 실행 중 만든 함수 displayName에서 name을 참조하는 건 이상하지 않다. 근데 이 displayName을 리턴한 이후에도 계속해서 name을 참조하는 듯한 현상이 발생한다! MakeFunc의 실행이 끝나면 지역변수였던 name에 더 이상 접근할 수 없으리라 예상하는 것이 당연한 수순같지만 그렇지 않은 것.

 

이는 바로 자바스크립트가 클로저라는 것을 형성하기 때문이다. 이렇게 함수가 정의된 당시에 참조할 수 있었던 변수들을 계속 참조할수 있는 것을 클로저라고 한다. 위 예시의 경우 myFunc는 MakeFunc가 실행될 때 생성된 displayName이란 함수의 인스턴스에 대한 참조인데, displayName의 인스턴스가 변수 name과 함께 살았던(?) 때의 환경을 유지하기 때문에 이런 현상이 가능한 것. 당연한 소리지만 name변수가 자체가 안 죽고 살아 숨쉬는 것이 아니므로 console.log(name)을 하면 에러가 난다. 

 

좀 더 흥미로운 예제를 살펴보자. 

출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures

실로 골때리는 예제가 아닐 수 없다. 그와 동시에 재밌는 예제이다. 단일인자 한 개를 받아 새 함수를 리턴하는 makeAdder를 정의했는데, 이 새로 정의된 함수도 단일인자를 받아서 x, y, z의 합을 리턴한다. 중요한 것은 이 예제가 클로저를 형성한다는 것! add5는 5를 인자로 makeAdder를 호출해 결국 x = 5, y = 100이란 클로저를 이룬 상태에서 x + y + z를 리턴하는 함수 인스턴스를 참조하는 녀석이고, add10은 10을 인자로 makeAdder를 호출해 x = 10, y = 100이란 클로저를 이룬 상태에서 x + y + z를 리턴하는 함수 인스턴스를 참조하는 녀석이다. 

 


클로저 활용 - 캡슐화

그럼 이 클로저를 활용하는 예시를 살펴보자. 바로 객체 프로퍼티 등의 캡슐화에 이를 이용할 수 있다. 바로 다음처럼.

createUser함수 내부에서 user객체가 생성될 때 _email이란 지역변수를 참조할 수 있던 클로저를 형성한 채로 있기 때문에 user1._email처럼 직접 접근해 수정할려고 하는 dog억지 땡깡을 막을 수 있는 것.

 

파일들을 기능별로 분리하는 작업을 모듈화라고 하고, 각각의 파일들을 모듈이라고 부른다. ES6부터 이 모듈화를 할 수 있는 문법이 등장했는데 그것이 바로 export와 import이다. 어떻게 쓰는지 알아보자.

 

모듈 스코프

모듈이 되는 파일들은 독립적인 스코프를 가져야 하며, 모듈 파일이 갖는 스코프를 모듈 스코프라고 한다. 이는 모듈 파일 안에서 선언된 변수나 함수 등은 그 파일 안에서만 사용 가능해야 한다는 것을 의미한다. 일반적으로 HTML문서에서 <script>태그로 단순히 js파일을 불러오면 모듈 스코프가 적용되지 않지만, <script>태그에 type="module"이란 속성값을 주면 그 파일에 모듈 스코프를 줄 수 있다. 이 속성값을 통해 브라우저가 해당 스크립트가 모듈이란 것을 인식한다고 함.

참고 : 모듈은 로컬 파일에서 동작하지 않고, HTTP 또는 HTTPS 프로토콜을 통해서만 동작.
웹 서버 등을 통해 열어야지만 모듈이 정상적으로 동작함(vscode의 경우 live server등을 통해 이용 가능)

 

export, import

모듈스코프를 갖고 있으면 다른 파일에서 그 모듈 파일의 변수&함수 등을 쓸 수가 없음. 이 경우 특정 문법을 통해 모듈 파일의 변수나 함수를 사용 가능한데, 이를 가능하게 하는 것이 export이다. 이 export키워드를 통해서 모듈 파일에서 "너는 다른 데서도 쓸 수 있게 해줄게"라고 지정해주는 것이 가능한 것임. 그러나 단순히 export했다고 해서 다른 파일에서 아무런 제한없이 막 export된 애들을 쓸 수 있는 게 아니라, export된 변수나 함수 등을 쓰고자 하는 파일에서 import를 통해 한 번 더 불러오는 과정이 필요하다.

 

간단한 예시를 보이겠다.

a.js에서 export키워드를 통해 title변수와 func라는 함수를 export 즉 외부에서 쓸 수 있도록 지정했다. 이 export키워드가 붙지 않았다면 외부에서 쓸 수 없다. 이제 다른 js파일, 예를 들면 b.js에서 다음과 같이 import해서 이들을 쓸 수 있다.

title과 func를 a.js로부터 갖고 와 쓰겠다! 라는 의미로 해석하면 된다. 즉 a.js에서 export된 것들 중 내가 쓰길 원하는 애들만 따로 골라 그 목록을 중괄호 {}안에 써서 갖고 올 수 있다는 의미. 객체에서의 destructuring문법과 비슷하다고 볼 수 있다.

 

방금 전에선 export할 때 내보내고 싶은 변수나 함수 앞에 하나하나 export키워드를 붙이는 방법들을 살펴봤다. 근데 사실, export는 다음과 같은 방법들로도 사용 가능하다.

즉 export { name1, name2, ..., nameN}의 방법을 쓰면 내보낼 변수나 함수 앞에 하나하나 export를 붙일 필요 없이 한꺼번에 export할 수 있고, as키워드를 통해 이름을 바꿔서 내보낼 수도 있다는 것이다.

 

import는 다음과 같은 방법으로도 가능하다.

import할 모듈에서 export된 애들을 내가 원하는 이름의 객체로 바인딩해 가져올 수도 있고, export할 때 as로 이름 바꿔주던 것처럼 import할때도 이름 바꿔서 가져올 수도 있다. 그리고 import만 하면 (세 번째 방법) export된 애들 모두를 export된 이름으로 쓸 수 있게 되는 줄 알았으나 그게 아니었다. 단순히 그 특정 모듈 파일을 가져와 한 번 실행하고 끝나는 듯하다. 

 

음..함수에서 인자로 객체를 넘기는 상황에선, 함수 작성시 파라미터로 destructuring문법을 쓰면 인자로 받는 객체에서 원하는 property값들만 가져올 수 있었다. export한다는 것도 export하는 애들을 객체 형태로 묶어서 전달하는 것이고, 그렇기 때문에 import할때 destructuring문법처럼 원하는 애들만 가져올 수 있는 게 아닌가 생각한다. 이건 그냥 본인 뇌피셜임.

 

 

Named Exports, Default Exports

방금 전과 같이 여러 대상을 export하는 방식을 Named Exports라고 한다. 그러나 default키워드를 통해 하나의 대상만을 export하는 것도 가능한데, 이를 Default Export라고 한다.

 

모듈파일 내에서 default키워드는 딱 한 번만 사용 가능하며, export뒤에 default가 붙으면 반드시 하나의 대상만 내보낼 수 있게 된다. import하는 파일에서는 이렇게 내보내진 값을 default로 받을 수 있는데 as키워드를 통해 이름을 붙여서 받아야 한다. 

근데, 이 Default export방식의 장점은 import할 때 default값에 붙여준 이름을 중괄호 밖으로 빼주어 더 편하게 작성할 수 있다는 점이다. 즉 b.js에서 다음과 같은 방식으로 import가 가능해진다. 

즉 중괄호 여부로 default exports와 named exports를 구분할 수 있는 것. 

참고로 default exports와 named exports는 함께 사용 가능하다.

근데 이렇게 같이 섞어 쓰면 난잡하니까..보통 export할 대상이 여러 개면 named방식을, 한개일때만 default방식을 사용한다.

javascript에서 함수 작성 시 작성한 parameter의 수보다 더 많은 arguments를 써도 된다. 파라미터는 3개 받지만 실제 호출 시 인자들을 5개 7개 이렇게 써도 문제가 되지 않는다는 의미. 그러나 각 인자와 파라미터가 순서대로 매칭되고 남은 인자들은 낙동강 오리알 신세가 된다. 

 

그.러.나 우리의 자바스크립트는 친절하게도 함수 내에서 인자들로 받은 값들을 따로 다룰 수 있는 방법이 존재한다. 바로 arguments객체이다. 이 객체엔 우리가 인자로 전달한 값들이 유사배열 형태(즉 배열과 관련된 메소드는 못 씀)로 모조리 담겨있다. 낙동강 오리알 신세가 된 인자들까지 이걸로 다룰 수 있다. 

 

그러나 argument객체를 쓰는 방법은 유사배열을 활용하는 방법이라 아쉽게도 배열의 메소드를 쓰지는 못한다는 단점이 있다. ES6부터는 이런 점을 보완하는 방법으로 Rest Parameter를 지원한다. 특정 파라미터 앞에 ...을 붙인 것을 Rest parameter라고 부르는 것. 우선 다음을 보자.

동작 자체는 결국 방금 살펴본 arguments객체를 활용하는 방법과 다를 게 없어보인다. 그러나 위 방법은 내가 전달한 kim ~ cho까지의 인자들을 모두 args라는 파라미터로 받은 형태이다. 정확히는 배열 형태로. Rest Parameter는 그 이름답게 나머지 인자들을 모두 받는다는 점에서 강력한 힘을 가진다. 즉 다음과 같이 활용 가능

참고로 Rest Parameter는 나머지 인자들을 모두 자기가 가져가는 양심없는 놈이기 때문에..파라미터 작성 시 마지막 파라미터를 Rest로 해주는 것이 좋다. 맨 마지막 놈이 아닌데 Rest로 쓰는 건 의미가 없기도 하고

ul태그가 있고 얘가 li태그들을 자식들로 갖고 있다고 하자. 이 li태그들에 어떤 이벤트를 등록하려면 지금까지는 다음과 같이 해왔었다. 

<ul id="some-list">
    <li>가계부 정리하기</li>
    <li>자바스크립트 강의듣기</li>
    <li>리액트 공부하기</li>
    <li>코딩테스트 문제 풀기</li>
</ul>
const someList = document.getElementById("some-list");

for (let item of someList.children){
    item.addEventListener("click", function(e){
        e.target.ClassList.toggle("done");
    };
}

즉 자식들로 잡든 처음부터 getElementsByClassName으로 잡던 li태그들 하나하나에 직접 이벤트 리스너들을 때려박는 형태였다. 그러나 하나하나 때려박는다는 것이 성능상 안 좋기도 하고, 나중에 js코드로 some-list에 새로운 li태그를 넣을 경우 새로 넣어진 li태그에도 별도로 다시 저 이벤트를 때려박아야 하는 번거로운 일이 생긴다.

 

이런 상황에서 쓸 수 있는 것이 이벤트 위임.

간단하게 말해서, 자식들의 이벤트 리스너를 부모에서 다루는 것이다. 

이벤트 버블링을 통해서.

 

알다시피 이벤트 버블링은 자식에서 발생한 이벤트가 부모로 퍼져가는 것을 말한다. 즉 위와 같은 클릭 이벤트를 등록하는 상황에서, 자식(li태그)에서 발생한 click이벤트가 부모(ul태그 등)으로 퍼져나가는 것을 활용한다는 것이다. 그리고, e.target을 활용하면 이벤트를 발생시킨 녀석에 대한 특정한 동작 등이 가능하다!! 즉 다음과 같이 할 수 있다.

const someList = document.getElementById("some-list");

someList.addEventListener("click", function(e){
    if (e.target.tagName == "li"){
        e.target.ClassList.toggle("done");
    }
});

if (e.target.tagName == "li")를 넣은 이유는, ul태그의 영역에서 li태그가 없는 공간을 클릭하는 상황에도 저 이벤트 리스너가 수행되는 상황을 막는 것이다. 해석하자면 ul태그의 영역에 대해 클릭 이벤트가 발생했을 때 이벤트 리스너를 실행하는데 그 중에서도 ul 태그 내의 li태그가 클릭돼서 수행되는 경우에만 if문 내의 코드를 실행한다는 것. 

 

기존에 쓰던 반복문을 통해 하나하나 이벤트 리스너를 때려박는 방법이 비효율적이라곤 생각했으나 이렇게 쉬운 방법이 있을 줄 몰랐다. 강의를 듣던 중 관련된 질문에 "아니 이렇게 하면 부모에 등록되는 건데, 이 경우엔 자식에도 등록이 되어지는 거냐?"란 질문이 있던데, 내 생각은 "No. 아닙니다. 자식인 li태그를 클릭하면 캡처링 단계에서 수수숙 내려와 li태그 즉 사건을 불러일으킨 놈을 찍고, 다시 부모 요소들로 올라가는 버블링이 생기게 되는데 그 때 저 이벤트 리스너가 수행되면서 target을 통해 불러일으킨 놈이 내 자식 중에서 li일 때만 수행하는 방식입니다"이다.

 

참고로 이 이벤트 위임이란 방법은 버블링을 이용하는 방법이므로, 당연한 소리지만 자식 태그에 버블링을 막도록 코드를 짜놨으면 무용지물이다. 이 점 알아두고 사용하자.

js에서 sort()메소드를 사용하면 배열을 정렬할 수 있다. sort는 정렬한 배열을 리턴해주는데 주의할 점은 정렬한 배열은 새 배열이 아니라 원본 배열과 같은 녀석이다(즉 같은 참조이다).

 

sort()메소드에 아무런 인자도 전달해주지 않으면 기본적으로 유니코드에 정의된 문자열 순서에 따라 정렬해준다.

즉,

const letters = ['A', 'C', 'E', 'B'];
const nums = [111, 13, 20, 10000];

letters.sort();
nums.sort();

console.log(letters); // ['A', 'B', 'C', 'E'];
console.log(nums);    // [10000, 111, 13, 20]

숫자들조차도 문자들 정렬할 때처럼 사전순으로 정렬된 모습이다ㅋㅋ 즉 문자열을 정렬할 경우엔 그냥 sort()를 써도 되지만, 숫자들을 정렬할 때는 sort()만 한다고 되지 않는 모습. 파이썬은 그냥 해주던데..에휴

 

암튼 이렇게 숫자를 정렬하고 싶은 상황 등에선 sort()에 콜백함수를 전달해야 한다. 

const nums = [111, 13, 20, 10000];

nums.sort((a, b) => a - b); // 오름차순 정렬
console.log(nums);          // [13, 20, 111, 10000]

nums.sort((a, b) => b - a); // 내림차순 정렬
console.log(nums);          // [10000, 111, 20, 13]

급하면 여기까지만 보고 나가도 됨.

근데 이제 이 이유가 궁금한 사람들이 있잖아? 나처럼. 

다른 데 가지 말고 여기서 저렇게 해야 하는 이유를 같이 보자구요 :) ㅋㅋㅋㅋㅋㅋ

 

우선, sort()는 파라미터로 compareFunction을 옵셔널로 받음. 옵셔널이란 말은 이 놈을 받을 수도 있고 안 받을 수도 있다는 얘긴데 받는다면 이 compareFunction을 기준으로 정렬이 되고, 안 받는다면 유니코드에 의한 문자열 순서대로 정렬한다는 것.

 

암튼 이 compareFunction은 파라미터를 두 개 받도록 만들어야 함. (a, b)로 받는다고 해봅시다. 그러면 이 함수의 리턴값에 의해 어떻게 정렬할지가 결정되는데, 그건 다음과 같음.

 

1. 리턴값이 음수 : a가 앞으로 오도록 정렬

2. 리턴값이 0 : a, b를 바꾸지 않음

3. 리턴값이 양수 :  b가 앞으로 오도록 정렬

 

따라서 리턴값을 a - b로 하면 오름차순으로, b - a로 하면 내림차순으로 정렬할 수 있는 것임!

 

※ 참고링크

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

 

Array.prototype.sort() - JavaScript | MDN

sort() 메서드는 배열의 요소를 적절한 위치에 정렬한 후 그 배열을 반환합니다. 정렬은 stable sort가 아닐 수 있습니다. 기본 정렬 순서는 문자열의 유니코드 코드 포인트를 따릅니다.

developer.mozilla.org

 

+ Recent posts