5-3 클로저 활용 사례
다음은 클로저 활용 사례입니다.
먼저 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때 클로저를 활용할 수 있습니다. 위 코드를 봅시다.
fruits에는 배열 객체를 할당해주고 이를 순회하여 버튼을 만들었습니다. 각 버튼에는 clickHandler를 붙여주어 클릭 이벤트가 발생했을 때 해당 fruit 이름을 alert창으로 보여주게 됩니다.
forEach의 콜백함수가 끝나고 각 이벤트 핸들러에서 fruit 변수를 참조하기 위해 클로저가 발생하죠.
이번에는 아까 봤던 코드를 조금 수정해보겠습니다. 먼저 이벤트 핸들러에서 사용한 콜백 함수를 alertFruit로 분리시켜주었습니다. 그리고 bind 메서드를 이용해 this 바인딩과 fruit 매개변수를 직접 넘겨주었습니다. 값을 직접 넘겨주었기 때문에 클로저는 사용되지 않습니다. 하지만 이렇게 될 경우 우리가 원하는 this와는 달라집니다.
그래서 이번에는 고차함수를 사용해봅시다. 여기서 고차함수는 함수를 인자로 전달받거나 함수를 결과로 반환하는 함수입니다.
alertFruit 함수가 또 다른 함수를 리턴합니다. 리턴하는 함수에서 fruit을 참조할 때 해당 environmentRecord에서 값을 찾을 수 없으므로 상위 컨텍스트의 lexical environment에서 값을 찾을겁니다. 즉, 여기서 클로저가 발생하죠.
다음은 접근 권한을 제어할 때 (정보 은닉) 클로저를 사용할 수 있습니다.
정보 은닉이란 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화하는 것을 의미합니다. 이를 통해 모듈간 결합도를 낮추고 유연성을 높일 수 있습니다.
public, private, protected와 같은 접근 제한자가 있습니다. 자바스크립트는 기본적으로 변수 자체에 접근 권한을 직접 부여할 수 있도록 설계되지 않았습니다. 하지만 클로저를 이용하면 함수 차원에서 public과 private한 값을 구분하는 것이 가능합니다.
아까 봤던 예시중 하나인데요. 이 코드에서도 inner함수를 return 함으로써 a의 값을 외부에서도 읽을 수 있습니다. 이게 유일한 수단이죠.
즉, 외부에서 볼 수 있게 하려면 return을 하고 내부에서만 사용할 정보들은 return 하지 않는다! 라고 보셔도 될것 같습니다.
또 다른 예시인데요. 만약 위와 같은 규칙을 갖는 게임을 한다고 해봅시다.
위와 같은 car 객체를 만들어서 각 플레이어들에게 나눠주면 됩니다.
하지만 이는 큰 문제가 있습니다. 플레이어들이 마음대로 fuel, power, moved 변수의 값을 변경할 수 있게 됩니다. 그 외 다른 행동들도 할 수 있죠. 만약 이러한 상태로 게임이 진행된다면 큰일날겁니다.
그래서 객체를 return 해주는 코드를 수정하였습니다. fuel, power 변수는 외부에서 접근이 제한됩니다. private한 변수라고도 볼 수 있습니다. moved 변수는 getter를 부여해 읽기만 가능합니다.
하지만 이 코드도 한가지 문제가 있죠. 바로 run 함수는 덮어씌울 수 있다는 겁니다.
이 코드처럼 car.run에 새로운 함수를 할당해줄 수 있습니다.
따라서 Object.freeze() 메서드를 이용해 이 객체를 freeze! 못바꾸도록 해야합니다. Object.freeze()는 다시 쓰기 및 새로운 속성 추가 & 삭제가 안됩니다.
접근 권한을 제어하는 방법을 다시 정리해보겠습니다.
- 함수에서 지역변수 및 내부함수 등을 생성한다.
- 외부에 접근 권한을 주고자 하는 대상들로 구성된 참조형 데이터를 return 한다.
- return한 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 된다.
클로저를 활용해 부분 적용 함수도 만들 수 있습니다.
여기서 부분 적용 함수란 n개의 인자를 받은 함수에 미리 m개의 인자만 넘겨서 기억시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게 하는 함수입니다.
this 배울 때 봤던 bind 메서드도 부분 적용 함수입니다.
하지만 위 코드와 같이 사용하면 의도했던 것과는 달리 this 값이 변경되죠.
cf) 오타 수정 (8번째 줄을 아래 코드로 생각해주세요)
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
그러면 this에 관여하지 않는 별도의 부분 적용 함수를 만들어 봅시다. 여기서 partial이 바로 부분 적용 함수입니다.
addPartial에 partial(add, 1, 2, 3, 4, 5)를 할당했으니 originalPartialArgs는 add, 1, 2, 3, 4, 5가 되겠죠. 그리고 console.log(addPartial(6, 7, 8, 9, 10));
에서 restArgs에 6, 7, 8, 9, 10이 할당됩니다. 즉 1부터 10을 다 더해주는 함수가 실행되겠죠.
또한 여기서의 this는 실행 시점을 그대로 반영하게 됩니다. 즉, dog 객체에 있는 greet 함수에서 this는 dog를 의미합니다. greet 함수를 메서드로서 실행했으니까요!
실행 결과는 왈왈, 강아지 입니다! 가 나옵니다.
이 코드에서도 조금 아쉬운 부분이 있습니다. 바로 부분 적용 함수에 넘길 인자를 반드시 앞에서부터 전달해야 한다는 것이죠.
인자들을 원하는 위치에 미리 넣어놓고 나중에는 빈 자리에 인자를 채워넣어 실행하도록 해보겠습니다.
먼저 Object.defineProperty()에 대해 소개해드릴게요. Object.defineProperty는 객체에 새로운 속성을 직접 정의하거나 이미 존재하는 속성을 수정한 후 해당 객체를 반환합니다. 원형은 위와 같은데요. 여기서 obj는 속성을 정의할 객체, Prop은 새로 정의하거나 수정하려는 속성의 이름 또는 symbol, descriptor는 새로 정의하거나 수정하려는 속성을 기술하는 객체입니다.
즉 위 코드처럼 _라는 것을 window 객체에 정의를 해주었습니다.
가 있는 부분들은 무시되고 나중에 addPartial(10, 30)에서 에 해당 인자를 순서대로 할당해주게 됩니다.
디바운스에서도 사용될 수 있습니다. 디바운스는 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것을 말합니다.
처음 setTimeout이 실행되고 wait(ms)만큼 지나면 해당 콜백 함수가 실행됩니다. 그 이전에 debounce가 실행되면 clearTimeout으로 인해 무시됩니다. 즉, 각 이벤트가 바로 이전 이벤트로부터 wait 시간 이내에 발생하는 한 마지막에 발생한 이벤트만이 초기화되지 않고 실행됩니다.
다음은 커링 함수입니다. 커링 함수는 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것입니다. 커링은 한 번에 하나의 인자만 전달하는 것을 원칙으로 합니다. 중간 과정상의 함수로 실행한 결과는 그 다음 인자를 받기 위해 대기만 하고 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않습니다.
코드를 통해 살펴보겠습니다. curry3은 func, a, b 인자를 순차적으로 받는 커링함수입니다. getMaxWith10함수는 max함수와 a = 10 인자가 있게 됩니다. 따라서 출력은 위와 같이 10 25 가 됩니다.
만약 인자가 많아지면 어떻게 될까요?? 왼쪽 코드처럼 depth가 굉장히 길어지게 됩니다. 가독성도 많이 떨어지죠. ES6의 화살표 함수를 이용하면 오른쪽과 같이 간단하게 나타낼 수 있습니다.
화살표 순서에 따라 함수에 값을 차례로 넘겨주면 마지막에 func가 호출됩니다.
마지막 인자가 넘어갈 때까지 함수 실행을 미루는데 이를 함수형 프로그래밍에서는 지연 실행이라고 합니다.
또 다른 예시로 위와 같이 데이터를 fetch할 때 사용할 수 있습니다.
마지막으로 정리입니다.
클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상입니다. 내부함수를 외부로 전달하는 방법에는 함수를 return 하는 경우 뿐 아니라 콜백으로 전달하는 방법도 있습니다.
클로저는 특성상 기본적으로 메모리를 차지하게 됩니다. 따라서 프로그래머가 메모리 관리를 해주어야 합니다.
이상 발표를 마치겠습니다. 감사합니다.