본문 바로가기

3.구현/HTML5&Javascript

[javascript] 자바스크립트 중급자는 이것만 알고가자 2

들어가기

중급자라면 이정도는 알고 있으면 좋다고 생각하는 두번째 부분을 모았습니다.

작성자: ospace114@empal.com, http://ospace.tistory.com/

컨테이너

const foo = { name:'FOO' };
const bar = { name: 'BAR' };

const m = new Map();
m.set(foo, 'foo');
m.set(bar, 'bar');
m.set(foo, 'foo')
 .set(bar, 'bar'); // 체인 형태

if (m.has(foo)) console.log("foo existed.");

순회

for(let u of m.keys()) console.log(u.name);

for(let r  of m.values()) console.log(r);

for(let ur of m.entries()) console.log(`${ur[0].name}: ${ur[1]}`);

for(let [u,r] of m.entries()) console.log(`${u.name}: ${r}`);

for(let [u,r] of m) console.log(`${u.name}: ${r}`);

삭제

m.delete(foo);
s.delete(10);

초기화

m.clear();
s.clear();

위크맵(WeakMap)

WeakMap과 Map과 차이점이다.

  • 키는 반드시 객체
  • WeakMap의 키는 가비지 콜렉션에 포함됨
  • WeakMap은 iterable이 아니면 clear() 메소드도 없음

객체 인스턴스 전용(private) 키를 저장하기에 적합하다.
Map을 써도되지만 그렇게 하면 SecretHolder 인스턴스에 저장한 내용은 가비지 콜랙션에 포함되지 않는다.

const SecretHolder = (function() {
    const secrets = new WeakMap();
    return class {
        setSecret(secret) {
            secrets.set(this, secret);
        }
        getSecret() {
            return secrets.get(this);
        }
    };
})();

const a = new SecretHolder();
const b = new SecretHolder();
a.setSecret('secret A');
console.log(a.getSecret());

셋(Set)

중복을 허용하지 않는 데이터 집합이다.

const roles = new Set();
roles.add('User');
roles.size;
roles.delete('User');

위크셋(WeakSet)

객체만 포함할 수 있으며 가비지 콜렉션 대상이 된다. 단순히 객체가 셋 안에 존재하는지 여부만 확인하는 용도이다.

예외와 예외처리

Error 객체

Error 객체를 생성해서 던져야 좀더 자세한 에러 정보를 확인할 수 있다.

function a() {
    b();
}

function b() {
    c();
}

function c() {
    throw new Error('c error');
}
try {
    a();
} catch(err) {
    console.log(err.stack);
}

이터레이터(Iterators)

이터레이터는 순회를 좀더 편하게 만들기위한 도구이다. 이를 사용하게 되면 for 루프를 더 단순하게 사용할 수 있다.

const arr = [1, 2, 3, 4, 5];
const it = arr.values();
let current = it.next();
while(!current.done) {
    console.log(current.value);
    current = it.next(); 
}

array에서 이터레이터를 간단하게 사용할 수 있다.

class Log {
    constructor() {
        this.messages = [];
    }
    add(message) {
        this.messages.push({ message, timestamp: Date.now() });
    }
    [Symbol.iterator]() {
        return this.messages.values();
    }
}

const log = new Log();
log.add('first msg');
log.add('2nd msg');

for(let each of log) {
    console.log(`${each.message} @ ${each.timestamp}`);
}

이터레이터 프로토콜 사용하면 복잡한 데이터도 순회할 수 있다.

class Log {
    constructor() {
        this.messages = [];
    }
    add(message) {
        this.messages.push({ message, timestamp: Date.now() });
    }
    [Symbole.iterator]() {
        let i = 0;
        const messages = this.messages;
        return {
            next() {
                if (i >= message.length)
                    return {value: undefined, done: true};
                return { value:messages[i++], done: false};
            }
        }
    }
}

제너레이터(Generator)

제너레이터는 이터레이터를 사용해 자신의 실행을 제어하는 함수이다.

일반 함수와 다른 점

  • 언제든지 호출자에게 제어권을 넘길 수 있음(yield)
  • 호출 즉시 실행되지 않고, 이터레이터를 반환하고, next()를 호출함에 따라 실행됨

제너레이터 만들때에는 function 뒤에 애스터리스크(*)을 붙인다. 그리고 return 외에 yield 키워드를 사용할 수 있다.

function* rgb() {
    yield 'red';
    yield 'green';
    yield 'blue';
}

for(let color of rgb()) {
    console.log(color);
}

제너레이터와 호출자 사이에 양방향 통신은 yield에 의해서 이루어지며, yield 표현식의 반환 값은 호출자가 제너레이터의 이터레이터에서 next() 호출시 제공되는 매개변수가 넘겨지게 된다.

function* interrogate() {
    const name = yield 'What is your name?'; // (1)
    const color = yield 'What is your favorite color?'; // (2)
    return `${name}'s favorite color is ${color}.`; // (3)
}

const it = interrogate();
it.next(); // (4)
it.next('Foo'); // (5)
it.next('Blue'); // (6)

제너레이터 호출하면 이터레이터 획득하고, (4) next()호출시 제너레이터 첫행(1)을 실행한다. yield 표현식으로 제너레이터는 호출자에게 제어권 넘기고 첫번째 행 실행을 완료된다. (5) next()를 다시 호출해야 하고, name은 next()에서 전달받은 값으로 실행한다. 즉, next()에 의해 'Foo'가 전달받고 해당 값을 name에 저장되고 다음 행(2)이 실행된다. 계속해서 yield에 의해 제어권이 넘어가고 다시 (6) next() 호출에 의해서 'Blue'가 전달되고 color에 저장된다. 그리고 다음 행이 실행되고 값이 리턴된다.

제너레이터와 프라미스 결합

제너레이터와 프라미스를 결합하여 비동기 코드를 효율적으로 관리 가능하다.

nfcall()은 함수 콜백을 프라미스로 호출하고 반환한다.

function nfcall(f, ...args) {
    return new Promise(function(resolve, reject) {
        f.call(null, ...args, function(err, ...args) {
            if (err) return reject(err);
            resolve(args.length<2? args[0] : args);
        });
    });
}

ptimeout()은 프라미스로 타임아웃을 호출한다.

function ptimeout(delay) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, delay);
    });
}

grun()은 제너레이터 실행기이다.(kyle simpson 글 참조)

function grun(g) {
    const it = g();
    (function iterate(val) {
        const x = it.next(val);
        if(!x.done) {
            if(x.value instanceof Promise) {
                x.value.then(iterate).catch(err => it.throw(err));
            } else {
                setTimeout(iterate, 0, x.value);
            }
        }
    })();
}

이를 사용한 node.js 샘플을보자. 프라미스를 사용한 비동기 처리보다 단순해진다.

function * theFutureIsNow() {
    const dataA = yield nfcall(fs.readFile, 'a.txt');
    const dataB = yield nfcall(fs.readFile, 'b.txt');
    const dataC = yield nfcall(fs.readFile, 'c.txt');
    yield ptimeout(60*1000);
    yield nfcall(fs.writeFile, 'd.txt', dataA+dataB+dataC);
}

grun(theFutureIsNow);

Promise.all()을 사용해보자.

function * theFutureIsNow() {
    cont data = yield Promise.all([
        yield nfcall(fs.readFile, 'a.txt'),
        yield nfcall(fs.readFile, 'b.txt'),
        yield nfcall(fs.readFile, 'c.txt')
    ]);
    yield ptimeout(60*1000);
    yield nfcall(fs.writeFile, 'd.txt', data[0]+data[1]+data[2]);
}

예외 처리를 추가해보자.

function * theFutureIsNow() {
    let data;
    try {
        data = yield Promise.all([
            yield nfcall(fs.readFile, 'a.txt'),
            yield nfcall(fs.readFile, 'b.txt'),
            yield nfcall(fs.readFile, 'c.txt')
        ]);
    } catch(err) {
        console.error('reading file');
        throw err;
    }
    yield ptimeout(60*1000);
    try {
        yield nfcall(fs.writeFile, 'd.txt', data[0]+data[1]+data[2]);
    } catch(err) {
        console.error('writing file');
        throw err;
    }
}

제너레이터 실행기를 제공하는 라이브러리가 있다.

지금은 async/await를 사용하고 있어서 promise사용이 더 단순해졌다.

IIFE와 비동기 코드

for 루프와 setTimeout에 의해 호출할 경우 변수 사용에 주의가 필요하다. 가급적 let 사용을 추천한다.

var i;
for(i=5; i>=0; --i) {
    // 종료시점에 i가 -1이 되면서 원하지 않은 호출 발생.
    setTimeout(function() { console.log('>>', i) }, (5-1)*1000);
}

이를 회피하기 위해 클로저를 사용해야하지만 이를 단순하게 처리하기 위해서는 아래 처럼 사용하면 된다. let을 사용해서 루프마다 변수 i의 복사본을 새로 만든다.

for(let i=5; i>=0; --i) {
    setTimeout(function() { console.log('>>', i) }, (5-1)*1000);
}

콜백(callback)

node.js에서 프라미스를 사용하지 않는다면 err가 처음 인자로 에러 여부를 판단하는 형태의 콜백 형태를 갖는다. 이를 error-first callback이라고 한다.

fs.readFile('foo', function(err, data) {
    if(err) return console.error('error reading file');
    console.log('data:', data);
}

콜백 사용에 있어서 주의할 부분이 중첩된 콜백 호출에 의한 콜백헬이다. 아래 예제는 readFile()에 의해 호출되는 콜백은 익명함수이기에 외부 try의 범위를 벗어나서 붙잡을 수 없다.

try {
    fs.readFile('foo', function(err, data) {
        if(err) return throw err;
    }
} catch(err) {
    console.log('error reading file');
}

이벤트

가장 먼 조상부터 이벤트 응답 시작하는 방식을 캡처링(capturing)이라고 하고, 이벤트가 발생한 곳에서 거슬러 올라가는 방식을 버블링(bubbling)이라 한다.

preventDefault()는 이벤트를 취소하는 함수이고, 취소된 이벤트(defaultPrevented가 true)를 전달한다. 브라우저 이벤트 핸들러는 defaultPrevented가 true인 경우는 아무것도 하지 않는다. 사용자 이벤트 핸들러는 이를 무시하고 실행할 수 있다.

다른 방법으로 stopPropagation()으로 이벤트를 전달하지 않는다.

가장 강력한 방법으로 stopImmediatePropagation()으로 현재 요소에 연결된 이벤트 핸들러도 중지한다.

addEventListener()은 구식 'on' 속성(onClick)을 대처하기 위해서 사용한다. 'on'속성은 핸들러 하나만 등록하는 단점이 있다.

이벤트 카테고리

이벤트 종류는 다음과 같다.

  • 드래그 이벤트 (dragstart, grag, dragend, drop)
  • 포커스 이벤트 (focus, blur:다른 곳으로 빠져나올때, change)
  • 폼 이벤트(submit)
  • 입력장치 이벤트(mousedown, move, mouseup, mouseenter, mouseleave, mouseover, mousewheel, keydown, keypress, keyup)
  • 미디어 이벤트(pause, play)
  • 진행 이벤트(load, error)
  • 터치 이벤트(touches)

객체 보호

생성된 객체 내부를 보호하는 기능으로 동결, 봉인, 확장 금지가 있다.

동결

동결은 Object.freeze()를 사용하고 동결여부는 Object.isFrezen()를 사용한다.

동결되면 다음 작업 불가능하다.

  • 속성 값 수정 또는 할당
  • 속성 값 수정하는 메소드 호출
  • setter 호출
  • 새 속성 추가
  • 새 메소드 추가
  • 기존 속성이나 메소드 설정 변경

봉인

봉인은 Object.seal()를 사용하고 봉인여부는 Object.isSealed() 사용한다.
봉인하면 다음 작업이 불가능하다.

  • 새 속성을 추가
  • 기존 속성 변경 및 삭제 불가

확장금지

확장금지는 Object.preventExtensions()이고 확인은 Object.isExtensible()를 사용한다.
확장금지는 다음 작업이 불가능하다.

  • 새 속성 추가 불가

프락시

프락시는 ES6에 추가된 메타 프로그래밍 기능이다.

const foo = {a:1, c:3};
const betterFoo = new Proxy(foo, {
    get(target, key) {
        target[key] || 0; // 값이 있으면 리턴하고 아니면 기본 0을 리턴.
    }
});

betterFoo.a; // 1
betterFoo.b; // 0
betterFoo.c; // 3

참고

  1. 모질라 MDN
  2. Mark Pilgrim, Dive Into HTML5
  3. ES6은 ECMA-262 ECMAScript 2015 언어 명세
  4. JavaScript Weekly, http://javascriptweekly.com
  5. Node Weekly, http://nodeweekly.com
  6. HTML5 Weekln, http://html5weekly.com
  7. 악셀 라우슈마이어(es6관련), http://www.2ality.com
  8. Nolan Lawson(자바스크립트개발자), http://nolanlawson.com
  9. David Walsh(es6 generator), https://davidwalsh.name
  10. @kangax, http://perfectionkills.com
반응형