원문
Csaba Hellinger, http://dealwithjs.io/es6-features-10-use-cases-for-proxy/
오늘 우리는 ES6의 기능 중 하나 인 Proxy의 사용 사례를 살펴볼 예정이다.
소스 코드는 jsProxy Repository의 GitHub에서 찾을 수 있다.
여러분은 지금까지 ES6 Proxy에 대해 들어 봤거나 사용해 보았을 것이다. Proxy가 무엇인지, 어떻게 사용하는지에 대한 많은 정보가 있지만, 필자가 실제(와 같은) 사용 사례를 찾으려고 노력했고, Proxy가 어떤 곳에 쓰이는지 작은 정보를 가지고 왔다.
프락시는 모든 최신 브라우저에서 지원된다. (힌트 : Internet Explorer는 최신 브라우저가 아니다.)
이 글의 코드는 꽤 무거워서 평소처럼 전체 소스를 여기에 넣지는 않았다. 하지만 모든 섹션의 시작 부분에 관련 소스에 대한 링크를 삽입해 두었다. 하지만 index.html을 열어서 한 번에 확인해보고 모두 사용해 볼 수 있다.
몇 가지 예제는 오류를 던지고 있다. 이것은 의도적인 오류 던지기다. 오류가 예상되는 행은 try-catch 블록으로 쌓여 있고 주석으로 표시되어 있다.
설명으로 들어가기 전에, 구문과 사용법에 대해 조금 더 자세히 살펴보자.
소스 코드 : 00-basic-example.js
프락시는 속성 값을 가져오는 것과 같이 기본 작업을 가로 채기 위해 객체를 래핑 하는 방법이다. 낚아채려는 작업에 대한 트랩이 있는 처리기 객체를 제공할 수 있다. 트랩을 정의하지 않은 작업은 원본 객체로 전달된다.
예제에서 proxy.a를 요구할 때, 우리 handler.get 트랩이 호출될 것이다. 원래 객체로 호출을 전달하기 전후에 원하는 모든 작업을 수행할 수 있다. 물론 호출을 전달할 필요가 없으며 완전히 다른 작업을 수행할 수 있다.
Proxy 사용자는 원래 객체에 직접 액세스할 수 없으므로 캡슐화, 유효성 검사, 액세스 제어 및 기타 여러 가지 유용한 도구로 사용할 수 있다. 재미있는 예제들을 보려면 계속 읽으면 된다.
그럼, 우리가 이것을 어떤 방식으로 사용할 수 있는지 보자.
소스 코드 : 01-prop-defaults.js
첫 번째 실제 예는 객체에 기본값을 추가하는 것이다. 객체에 없는 키를 요청하면 defaults가 제공한 객체에서 해당 값을 가져온다. 하지만 in
연산자는 여전히 해당 키가 없다고 판단할 것이다. 나중에 확인해 볼 것이지만, 우리는 이것 또한 트랩으로 정의할 수 있다. 이 예제에서는 클라이언트 코드가 실제 값과 기본값을 구별할 수 있기 때문에 올바른 동작이다.
당신은 아마 Reflect.get
호출을 눈치챘을 것이다. Reflect
모듈을 사용하여 객체를 프락시하지 않은 경우 원래의 함수를 호출할 수 있다. 앞의 예제에서와 같이 obj[prop]
를 사용할 수도 있지만 이 방법이 더 깔끔하고 프락시가 아닌 객체도 사용할 수 있는 동일한 구현을 사용한다. 이 방법이 get
의 동작과 같이 쉽게 복제할 수 있는 간단한 트랩에서는 필요하지 않다고 느껴지겠지만, ownKeys
처럼 복제하기 더 어렵고 오류가 발생하기 쉬운 구현들을 위해서 필자는 Reflect
를 사용을 습관화 하는것이 좋다고 생각한다.
소스 코드 : 02-private-properties.js
프락시는 속성에 대한 액세스를 제한하는 데에도 좋다. 이 경우에는 _
로 시작하는 속성을 숨기고 속성을 실제로 비공개로 만든다.
보다시피, 이를 달성하기 위해 정의해야 하는 다섯 가지 트랩이 있지만 모두 동일한 논리를 따른다. 우리는 호출을 전달하거나 속성이 없을 때는 제공된 filterFunc
를 요청한다.
하지만, 당신은 지금까지 Reflect
사용의 장점을 보았다. 이 모든 트랩에서 적절한 전달을 구현하는 데 더 많은 코드가 필요할 것이다.
한 가지 주목해야 할 것은 프락시에서 메서드를 호출 this
하면 원래 개체가 아니라 프락시가 기본적으로 프락시를 참조하므로 메서드는 private 속성에 액세스할 수 없게 된다. get
트랩의 원래 객체에 메서드를 바인딩 하여 이 문제를 해결할 수 있다.
소스 코드 : 03-enum-basic.js
많은 자바 스크립트 코드는 문자열, 일반 객체 또는 고정 객체를 enum으로 사용한다. 여러분이 알다시피, 이 설루션은 타입 안전 문제가 있으며 일반적으로 오류가 발생하기 쉽다.
프락시는 실행 가능한 대안을 제공할 수 있다. 우리는 평범한 키-값 객체를 받으며, 실수로 가하는 수정으로부터 보호한다. Object.freeze
보다 더 강할 것이다(이 수정을 허용하지만 오류는 발생하지 않으므로 암묵적인 버그가 발생할 수 있다).
소스에는 일반 객체, 고정 객체 및 enum Proxy가 클라이언트 코드에서 수행할 수 있는 것과 동일한 실수를 처리하는 방법을 보여주는 세 개의 섹션이 있다.
우리는 한 걸음 더 나아가서 "시뮬레이션 된"방법으로 enum을 보완하기 위해 Proxy를 사용할 수도 있다. (이 메서드는 실제로는 존재하지 않지만 Proxy에서는 컨텍스트 바인딩을 이용해 실제 내부에 메서드가 존재하는 것처럼 사용할 수 있다.) 예를 들어 enum의 이름에서 값을 가져오는 메서드를 추가 할 수 있다.
소스 코드 : 03-enum-nameof.js
일반적인 방법으로 일반적인 프로토 타입 상속을 사용하여 메서드를 추가 할 수 있지만, Object.keys예를 들어 enum의 키를 얻을 때 메서드의 이름이 표시된다. 또한, 필자는 이렇게도 구현할 수 있음을 보여주고 싶었다.
소스 코드 : 04-onchange-object.js
프락시는 객체로 발생하는 이벤트를 구독하는 데에도 유용하다. 이 예제에서는 모든 것을 원본 객체로 전달하지만 속성을 설정하거나 삭제 한 후에도 onChange
이벤트 핸들러를 호출한다.
같은 방법으로 다른 이벤트도 구현할 수 있다. 예를 들어 onValidate
는 변경 사항을 적용하기 전에 변경 사항을 확인하기 위한 이벤트이다.
이 패턴은 배열에서도 유용하다. length
처럼 항목 변경 외에도 속성 변경을 확인할 수 있다.
소스 코드 : 05-proxy-as-handler.js
프락시를 사용하여 객체에 저장하는 값도 제어할 수 있다. 이 값은 메타 데이터를 값과 함께 저장해야 하는 경우에 유용하다. 이 예제에서는 TTL(time to live)을 실제 값 옆에 넣으므로 cache.a = 42
대신에 속성을 설정할 때 {ttl: 30, value: 42}
처럼 저장한다. (물론 반대로 속성을 받을 때도 해야 한다.) 그런 다음 ttl을 초마다 감소시키고 0에 도달하면 속성을 제거한다.
덧붙여, TTL을 함수에서 다른 값과 다른 속성이름으로 받을 수 있다. 예를 들어 1분 동안 설정값을 캐시 할 수 있지만 Ajax 응답은 10초가 걸리는 경우가 있다.
물론 TTL을 별도의 객체에 저장하여 얻을 수도 있지만, 필자는 이 방법이 좀 더 우아한 해결책이라고 생각한다. 필자는 이 "인라인 메타 데이터" 의 가능성을 보여주고 싶었고 이는 많은 상황에서 유용하다. 한 가지 염두 해 둘 것은 객체는 어떤 데이터베이스의 인터페이스가 될 수 있고, 메타데이터를 이용해 관리되는 데이터를 저장하는 것처럼, 데이터가 저장될 때나 수정될 때 프락시를 이용해서 데이터베이스에 반영할 수 있다는 것이다.
소스 코드 : 06-array-in.js
프락시를 사용하여 제한된 연산자 오버 로딩을 수행할 수 있다. 하지만 어떤 연산자들은 오버로드 할 수 없다(in
, of
, delete
, new
). 우리는 new
를 다음 예제에서 오버로드 할 것이다.
하지만 이번에는 in
연산자를 낚아채서 Array.includes
처럼 사용할 것이다. 이 연산자는 배열에 값이 있는지 확인한다.
이 방법은 아마도 실제 프로젝트에서는 별로 유용하지 않지만, 필자는 이 방법이 멋지다고 생각한다.
소스 코드 : 07-singleton-pattern.js
프락시로 싱글 톤 패턴(및 다른 생성 패턴)을 구현할 수 있다. 이 예제에서는 construct
트랩을 사용하여 new
를 사용해도 매번 싱글 톤 인스턴스를 반환하도록 할 것이다.
Java Script에서 싱글 톤을 사용하는 것은 논란의 여지가 있다. 많은 개발자들은 Dependecy Injection을 사용하기 때문에 쓸모없는 디자인 패턴이라고 생각한다. 그럼에도 불구하고 프락시를 사용하여 객체 생성을 트랩 하는 방법을 보여주는 것은 쉽고 간단하다. 그리고 팩토리, 빌더 메서드 등에 대해서도 같은 방법을 사용할 수 있다.
소스 코드 : 08-revocable-validated.js
프락시를 사용하여 속성과 속성 값을 개체에 추가하기 전에 유효성을 검사할 수 있다. 클라이언트 코드에서 몇 가지 옵션을 받는 라이브러리를 만들고 있다고 가정해 보겠다. 그래서 이벤트 핸들러가 호출되었을때 빈 옵션 객체 (또는 기본값으로 미리 채워진 객체)를 전달하고 클라이언트 코드는 옵션을 설정한다. 하지만 클라이언트가 전달한 옵션에서 유효한 옵션과 값만 받아들이고, 만약 그렇지 않으면 오류가 발생하도록 하려고 한다.
객체를 얻은 후에 유효성 검사를 수행할 수 있지만, 유효성 검사를 객체 자체에 내장하게 되면 얻을 수 있는 몇 가지 장점이 있다. 그 방법으로 인해 라이브러리 코드 내부는 객체가 항상 유효하고 안전하다고 가정할 수 있으므로, 객체를 사용하는 곳마다 유효성 검사 및 오류 처리 코드가 필요하지 않게 된다.
취소 가능한 프락시를 사용하여 클라이언트 코드의 액세스 권한을 취소할 수도 있으므로 이벤트 핸들러가 객체를 반환 한 후에는 수정할 수 없다. 좀 더 일반적인 용어로, 수정을 위해 내부 객체를 외부 코드에 전달할 때 그 객체가 나중에 변경되지 않도록 내부 객체를 보호 할 수 있다.
소스 코드 : 09-cookie-object.js
이 예제에서는 쿠키 지속성을 사용하여 객체를 보강한다. 객체를 생성할 때 우리는 속성으로 쿠키를 로드하고, 그 이후부터 모든 변경 사항을 쿠키에 저장한다.
쿠키 대신에 실제 데이터베이스를 사용할 수도 있지만 일반적으로 비동기 API를 가지고 있기 때문에 값 대신 Promise 를 반환할 수밖에 없고, 이로 인해 클라이언트 코드가 어렵게 변하게 된다. 아마도 console.log(dbObject.x, dbObject.y)
대신에 Promise.all([dbObject.x, dbObject.y]).then(console.log)
를 작성해야 할 것이다. 그래서 이 프락시는 실제 database API를 사용하는 것보다 전혀 간결하지 않을 수 있다.
하지만 이 부분에 대해서 깊게 생각할 필요가 있다고 생각한다. 필자는 이와같은 API가 멋지다고 생각하기 때문이다.
소스 코드 : 10-python-slicing.js
파이썬에 익숙한 사람들은 아마도 list slicing 구문을 좋아할 것이고, 필자도 자바 스크립트에서 비슷한 것을 갖고 싶어 했다.
익숙하지 않은 사용자를 위해 Python에서는 다음을 수행할 수 있다.
list[start:end:step]
구문을 사용하여 목록 (배열)의 하위 집합을 가져온다. 예를 들어, list[10:20:3]
는 10번째에서 20번째까지의 항목 중에서 다음 세 번째 항목들을 가져올 수 있다. (참고 : end
는 배타적이므로 실제로는 항목 10-19로 진행된다.)-1
번째는 마지막 항목이며, -2
번째는 마지막에서 두 번째이다.물론 Java Script로도 똑같이 할 수는 있지만 간결하고 세련된 방식으로는 할 수 없다. 하지만 여러분이 지금까지 봐왔다시피 프락시를 이용해서 구현해 보자.
Java Script 구문은 배열 인덱스에 콜론을 넣을 수 없지만 문자열을 사용하면 상관없다. 그래서 우리는 앞의 예제와 같이 list["10:20:3"]
처럼 쓸 수 있다. 그리고 get
트랩에 인덱스가 숫자이면 호출을 전달하고, 인덱스가 문자열이면 결과를 반환한다.
이 예제에서는 특별한 것이 없지만 대부분의 코드는 분할 논리이다. 프락시 핸들러 자체는 약 3줄의 코드이며 get
트랩 만 있다.
소스 코드 : 11-proxy-as-handler.js
이 예제는 유용하지는 않지만, 필자가 빼놓고 싶지 않은 재미있는 패턴이므로 보너스 11번째 예제로 추가했다.
여기서 우리는 Proxy 객체(logProxy
)를 사용하여 다른 Proxy(myObj
)에 대한 핸들러로 작동하여 모든 트랩을 동적으로 생성한다. 그래서 우리가 myObj.a = 3
할 때, 보통은 핸들러의 set
트랩이 작동한다. 그러나 handler(logProxy
)가 Proxy 자체이기 때문에 핸들러의 get
트랩은
set
트랩을 가져오기 위해 다음과 같이 logHandler.get(logProxy, 'set')
가 실행된다. 거기에서 Reflect[trap]
호출 전후에 로그를 넣어 동적으로 생성 한 관련 트랩을 생성하고 반환한다. 읽기만 하면 혼란스러울 수 있으므로 아래에 호출 상황을 보여주는 그림이 있다.(역자 주: 이미지 링크의 이미지를 찾을 수 없는 상태라서 이미지가 나오지 않습니다.)
필자는 단지 하나의 용도로만 생각한다 : 만약 당신이 모든 트랩이 똑같이 보이도록 Proxy를 만들고 싶다면 (Reflect 호출 전후의 동일한 코드), 여러분은 한 묶음의 상용구 코드를 사용하지 않고 생성할 수 있을 것이다.
보시다시피, 특히 이런 이중 프락싱은 더 느리다(놀라운 일은 아니지만), 따라서 프로덕션 코드에서는 이 프락시를 사용하지 않는 것을 추천한다.
소스 코드 : performance.js
나는 이 작은 성능 측정을하고 3개의 최신 브라우저에서 실행했다. (2015 MacBook Pro 기준)
결과 :
객체/프락시 유형 | Chrome (ms) | 사파리 (ms) | Firefox (ms) |
---|---|---|---|
일반 오브젝트 | 623 | 764 | 1510 |
No-op 포워딩 프락시 | 1506 | 1770 | 1950 |
Reflect를 사용한 프락시 | 3335 | 4531 | 8435 |
동적 핸들러로서의 프락시 | 5626 | 6005 | 10947 |
브라우저들 사이에는 상당한 차이가 있지만, 일반적으로 더 추상적인 프락시 패턴일수록 훨씬 느리다. 필자의 의견으로는, 말도 안 되게 느리다고 생각한다. 필자는 추상화에 대한 일부 성능 충돌을 이해할 수 있지만, Reflect를 사용한 프락시는 대한 5-8배나 느리다는 것은 너무하다. 그러나 이것은 구현이 성숙 해짐에 따라 시간이 지남에 따라 향상될 것이다.
재미있는 점은 Firefox가 이 기능을(2015년 5월 이후) 가장 오래전부터 지원했다는 것이다. 테스트 결과에서 가장 느리다.
필자는 프락시는 우리의 무기고에서 유용한 도구라고 생각한다. 다양한 문제에 사용될 수 있으며 코드를 더 간단하고 읽기 쉽게 만들 수 있다. 그러나 우리는 명심해야 한다. 객체가 동작하는 방식을 낮은 수준에서 변경할 때 코드의 다른 부분에서의 사용과, 동료 개발자들도 이에 대해 혼동하지 않도록 개발해야 한다. 이러한 객체(및 패턴)를 적절하게 문서화해야 하며, 파일을 개별 파일로 분리하거나 심지어는 명명 규칙을 사용하여 이러한 객체가 특별하다는 것을 명시해야 한다.
성능과 관련해서는, 프락시를 프로덕션 코드에서 사용할 수 있지만 자주 사용되는 객체에는 사용할 수 없다. 옵션 객체 (예제 08처럼)는 한 번에 두 번만 호출을 허용하는 경우, 프락시가 성능에 미치는 영향은 무시할 수 있을 정도로 작다. 대조적으로, 나는 Angular $scope
, Redux Store
또는 비슷하게 많이 사용되는 객체에 Proxy를 두지 않을 것 이다. 프로덕션 코드에는 사용하지 않을 것이다. 흥미로운 실험이겠지만 말이다 ...;)
이 게시물을 읽는게 즐거웠다면, 공유하거나 정기적으로 업데이트 되는 Facebook Page같은 곳에서 like를 부탁한다. 아니면 필자의 페이지 @dealwithjs를 팔로우 할 수도 있다.
그럼 행복한 프락싱 하길 바란다