즐겁고 유익한 자바스크립트 프록시 사용방법

이 문서는 https://medium.com/dailyjs/how-to-use-javascript-proxies-for-fun-and-profit-365579d4a9f8 를 번역한 내용입니다.

How to use JavaScript Proxies for Fun and Profit

즐겁고 유익한 자바스크립트 프록시 사용방법

아직 많이 사용되지 않고 있는 자바스크립트 새로운 기능이 있다. (자바스크립트 프록시) 자바스크립트 프록시를 이용하여 기존 객체를 래핑하고 객체의 속성이나 메서드 접근하는 어떤 것이든 언터셉트 할 수 있다. 그것들이 존재 히지 않더라도,,

You can intercept calls to methods that do not exist

You can intercept calls to methods that do not exist

Hello World Proxy

자 이제 시작이야. 내꿈을~ 내꿈을 위한 피카츄

다음 hello world 예제를 봅시다.

const wrap = obj => {
    return new Proxy(obj, {
        get(target, propKey) {
            console.log(`Reading property "${propKey}"`)
            return target[propKey]
        }
    })
}

const object = { message: 'hello world' }
const wrapped = wrap(object)
console.log(wrapped.message)

다음은 output

Reading property "message"
hello world

위 예제에서 속성/메서드에 접근하기 전에 단순히 어떤 행위를 했을 뿐이다. 그렇지만 반환 값은 원래 속성 또는 메서드를 반환하였다.

또한 set 핸들러를 구현 하여 속성 변경을 인터셉트 할 수 있다.

이는 속성을 검증하거나 그런 관련 된 것들에 유용 할 수 있다. 하지만 이것보다 훨씬 더 유용하게 사용 할 수 있다고 생각 된다. 나는 핵심기능을 위해 프록시를 사용 하는 새로운 프레임워크가 나오길 희망한다. 나는 관련해서 생각해본게 있으며, 다음 몇가지 아이디어가 있다.

An SDK for an API with 20 lines of code

말했듯이, 당신은 메서드가 호출 될 때 인터셉트 할 수 있습니다. 심지어 존재하지도 않는 것 까지.. 누군가가 프록시 객체에 메소드를 호출 할 때 get 핸들러가 호출 되고, 그러면 동적으로 생성된 함수를 반환 할 수 있다. 만약 당신이 필요없다면, 프록시를 만질 필요가 없다.

이 생각을 염두에 두고, 당신은 호출되는 메소드를 파싱 할 수 있고, 런타임에 그 기능을 동적으로 구현 할 수 있다. 예를 들어, api.getUsers()로 호출 할 때, GET /users API 를 만드는 프록시를 가질 수 있다. 이 컨벤션을 통해 우리는 api.postItems({name: 'Item name'})request body 로 첫번째 파라미터가 있는 POST /items 를 호출하는 것을 할 수 있습니다.

다음 전체 구현체를 봅시다.

const { METHODS } = require('http')

const api = new Proxy({}, 
{
    get(target, propKey) {
        const method = METHODS.find(method => 
            propKey.startWith(method.toLowerCase()))
        if (!method) return

        const path =
            '/' +
            propKey
                .substring(method.length)
                .replace(/[a-z]([A-Z])/g, '$1$2')
                .replace(/\$/g, '/$/')
                .toLowerCase()
        return (...args) => {
            cosnt finalPath = path.replace(/\$/g, () => args.shift())
            const queryOrBody = args.shift() || {}
            // 여기서 fetch 를 사용 한다.
            // return fetch (finalPath, { method, body: queryOrBody})
            console.log(method, finalPath, queryOrBody)
        }
    }
})

// GET /
api.get()
// GET /users
api.getUsers()
// GET /users/1234/likes
api.getUsers$Likes('1234')
// GET /users/1234/likes?page=2
api.getUsers$Likes('1234' , {page:2})
// POST /items with body
api.postItems({name: 'Item name'})
// api.foobar is not a function
api.foobar()

모든 메소드는 동적으로 구현되기 때문에, 프록시 객체는 단지 {} 이다. 실제로 기능 객체를 래핑 할 필요 없다. 당신은 $를 가진 몇몇 메서드를 보았을 것이다. 이는 인라인 파라미터를 위한 표시이다. 만약 당신이 이 방법을 좋아히지 않으면, 다른 것으로 바꿀 수 있다.

참고사항 : 위 예제를 최적화 할 수 있다. 매번 새로운 함수를 반환하는 것 대신에 hash객체에 동적으로 생성된 함수를 캐싱 할 수 있다.

📦 Querying data structures with more readable methods

people 배열을 가지고 있다면 할 수 있는 것

arr.findWhereNameEquals('Lily')
arr.findWhereSkillsIncludes('javascript')
arr.findWhereSkillsIsEmpty()
arr.findWhereAgeIsGreaterThan(40)

프록시로 할 수 있다!!! 우리는 쿼리를 수행 할 수 있는 그것 과 메소드 호출 파싱, 래핑된 배열을 프록시를 통해 구현 할 수 있다.

아래에서 몇몇 구현 가능 한것을 보자.

const camelcase = require('camelcase')

const prefix = 'findWhere'
const assertions = {
    Equals: (object, value) => object === value,
    IsNull: (object, value) => object === null,
    IsUndefined: (object, value) => object === undefined,
    IsEmpty: (object, value) => object.length === 0,
    Includes: (object, value) => object.includes(value),
    IsLowerThan: (object, value) => object < value,
    IsGreaterThan: (object, value) => object > value
}

const assertionNames = Object.keys(assertions)

const wrap = arr => {
    return new Proxy(arr, {
        get(target, propKey) {
            if (propKey in target) return target[propKey]
            const assertionName = assertionNames.find(assertion => 
                propKey.endsWith(assertion))
            if (propKey.startsWith(prefix)) {
                const field = camelcase(
                    propKey.substring(prefix.length,
                        propKey.length - assertionName.length
                    )
                )
                const assertion = assertions[assertionName]
                return value => {
                    return target.find(item => assertion(item[field], value))
                }
            }
        }
    })
}

const arr = wrap([
    { name: 'John', age: 23, skills: ['mongodb'] },
    { name: 'Lily', age: 21, skills: ['redis'] },
    { name: 'Iris', age: 43, skills: ['python', 'javascript'] }
])
console.log(arr.findWhereNameEquals('Lily')) // finds Lily
console.log(arr.findWhereSkillsIncludes('javascript')) // finds Iris

프록시를 사용하여 expect 같은 assertion 라이브러리 와 비슷 하게 작성 한다.

또다른 아이디어로 다음과 같은 query database 위한 API 라이브러리를 만들 수 있다.

const id = await db.insertUserReturningId(userInfo)
// Runs an INSERT INTO user ... RETURNING id

📊 Monitoring async functions

monitoring graph

메소드 호출을 가로 챌 수 있어서, 메소드 호출 할 때 promise 를 반환하면, promise 가 수행 될 때 시점을 추적 할 수 있다. 이 아이디어로, 나는 객체에 비동기 메소드를 모니터링 하거나, 커맨드 라인에 몇몇 통계적인 내용을 프린팅 하는 예제를 빠르게 만들 수 있다.

다음 아래와 같은 서비스가 있고, 이것을 래핑 햘 수 있는 하나의 메소드를 호출 한다.

const service = {
    callService() {
        return new Promise(resolve => 
            setTimeout(resolve, Math. random() * 50 + 50))
    }
}
const monitoredService = monitor(service)
monitoredService.callService() // we want to monitor this

아래는 전체 예제

const logUpdate = require('log-update')
const asciichart = require('asciichart')
const chalk = require('chalk')
const Measured = require('measured')
const timer = new Measured.Timer()
const history = new Array(120)
history.fill(0)

const monitor = obj => {
    return new Proxy(obj, {
        const origMethod = target[propKey]
        if (!origMethod) return
        return (...args) => {
            const stopwatch = timer.start()
            const result = orgiMethod.apply(this, args)
            return result.then(out => {
                const n = stopwatch.end()
                history.shift()
                history.push(n)
                return out
            })
        }
    })
}

const service = {
    callService() {
        return new Promise(resolve => 
            setTimeout(resolve, Math.random() * 50 + 50))
    }
}
const monitoredService = monitor(service)

setInterval( => {
    monitoredService.callService()
        .then(() => {
            const field = ['min', 'max', 'sum', 'variance', 'mean', 'count', 'median']
            const histogram = timer.toJSON().histogram
            const lines = [
                '',
                ...fields.map(field =>
                    chalk.cyan(field) + ': ' +
                    (histogram[field] || 0).toFixed(2))
            ]
            logUpdate(asciichart.plot(history, {height : 10}) + lines.join('\n'))
        })
        .catch(err => console.log(err))
},100)

자바스크립트 프록시는 슈퍼 파워풀하다. ✨

프록시는 약간의 오버헤드이지만, 런타임에 메소드를 동적으로 구현하는 기능을 가진 측면에서 슈퍼 엘레강스 하고 읽기 쉽게 만들어 준다. 나는 아직 벤치마크를 수행하지 않았지만, 프로덕션에서 사용할 계획이라면, 퍼포먼스 테스팅을 우선 해야한다.

그러나 비동기 함수를 디버깅하는 모니터링 개발에서 사용할 때는 문제없다.

Written on January 1, 2018