Javascript(Typescript) 의외로 자주보는 실수들.. 및 쓸데없는 팁들..

간만에 시간이 남아서 뭘 할까 생각 하다가, 저와 함께 작업하시던 분들이 흔하게 실수 하셨던 내용 몇가지 정리 해 보고자 합니다.

  • 예제는 경우에 따라 JS 혹은 Typescript로 작성 되어 있습니다.

글을 작성 하면서도, “에이 설마 저걸 모른다고? 저거 모르고 코드 작성을 어떻게 해?” 할만한 내용이 제법 있습니다. 실제로 프로젝트를 진행하며 경험한 개발자들의 실수를 모았음을 먼저 알려드립니다.

1. Promise return 경로 착오

function promiseGen(param: boolean): Promise<boolean> {
    return new Promise((resolve, reject) => {
        resolve(param);
    });
}

async function promiseTest(promise: Promise<boolean>): Promise<string> {
    await promise.then(result => {
        if(!result) 
            return 'fail1';
    }).catch(err => {
        return 'fail2';
    });

    return 'success';
}

async function main() {
    console.log(await promiseTest(promiseGen(true)));
    console.log(await promiseTest(promiseGen(false)));
}

main();

의외로 이 실수하는분들이 꽤 많았습니다. (실수라기보단, 실제로 동작을 모르는 경우였음)

‘fail1’, 'fail2’를 반환하는 부분은 실제로 anonymous function이며, 이 반환값은 다시 Promise의 반환값으로 연결이 되므로, 만약 ‘fail1’ 혹은 ‘fail2’ 반환부에 제어가 도달 하더라도 promise await 좌항으로 값을 받지 않고 있으므로 해당 값은 전혀 활용이 되지 못합니다.

본래 의도대로 작성을 하려면 promiseTest()가 아래와 같이 작성 되어야 합니다.

async function promiseTest(promise: Promise<boolean>): Promise<string> {
    return await promise.then(result => {
        if(!result) return 'fail1';
        return 'success';
    }).catch(err => {
        return 'fail2';
    });
}

2. async function의 잘못된 정의

적어도 C#에서 async/await를 사용 해보신 분들이라면 갸우뚱 할법한 내용입니다. C#에서는 아래와 같은 상황이 되면, compiler가 build time에 error를 발생 시키므로 코드 작성이 불가능 합니다.

다만, 우리의 친절한 (transpile이 포함된 환경의)JS/Typescript는 아래와 같은 경우를 아주 너그러이 허용 해 줍니다.

async function asyncTest() {
    console.log('00000000000');
}

function main() {
    asyncTest();
}

main();

비동기 처리 대상이 아님에도 asyncTest()라는 함수에 async 키워드를 붙여 사용하는 경우 입니다.
이게 뭐 그리 문제인가 싶겠습니다. 위 코드를 transpile 한 output은 아래와 같습니다.

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
function asyncTest() {
    return __awaiter(this, void 0, void 0, function* () {
        console.log('00000000000');
    });
}
function main() {
    asyncTest();
}
main();
  • transpile output의 일부만 발췌 되었습니다.
  • transpile output target에 따라 결과물이 다소 차이가 있을 수 있습니다.

현 수준의 transpiler는 function의 async 키워드만 보고 비동기 처리 대상으로 판단을 합니다. 위와같이 transpile output이 나온 경우, 비동기 처리 필요 없이 console.log()만 찍어도 되는 코드가 Promise로 한번 감싸지게 됩니다. Promise에 의해 불필요한 task 스위칭이 한번 발생 하게 됩니다.

뭐, 이정도로 미세한 성능 챙기려면 애초 왜 JS를 사용하느냐 의견이 있을 수도 있겠으며, 현실적으로 성능 깎아먹는건 의외로 사소한 비즈니스 로직들인경우가 많지만, 쓸데없는곳에 낭비되는 작은 리소스들도 잘 아껴야 하지 않겠는가가 제 의견 입니다.


분명 기억으로는 굉장히 많은 실수들을 목격 했던것 같은데 막상 정리해보려 하니 얼마 없네요… 뭐 대단하게 자주하는 실수들이라는 제목이 뻘쭘해서 쓸데없는 팁 몇개 더 던져봅니다.

3. bind는 봉인기다?

약간 논란이 있을 수 있기에, 목적부터 명확히 하자면, bind가 필요한 상황에서 이악물고 회피하지 않아도 되지 않겠는가 정도로 내용을 담습니다.

아주 옜날부터 저조한 성능으로 봉인되어왔던 bind()는, 반대로는 대표적 성능 개선 대상이기도 했습니다. 여러차례 성능이 개선 되어왔고, 현 시점에서 굳이 이 기능을 봉인해야 하는가 싶어 내용을 포함 했습니다.

4. Typescript에서 reflection이 불가능하다?

엄밀하게 언어/Engine level에서 reflection은 지원되지 않습니다. 헌데, JS 하는사람들은 타 프레임워크에서 좋은 개념을 뜯어와 적용하기 좋아하는 사람들이 참 많은듯 합니다. 마침 typescript에서 JAVA의 annotation, C#의 attribute에 해당되는 metadata라는것을 구현 했고, 여기에 reflect-metadata라는 package를 사용해 reflection을 흉내 낼 수 있습니다.

reflect-metadata는 내부적으로는 KV container에 불과 합니다. class 혹은 member에 metadata(annotation)을 붙이고, 해당 객체 + field name을 key 삼아 원하는 값을 넣고 필요할 때 불러 사용 할 수 있습니다. 코드 구현상으로는, 빌드(transpile) 시간에 KV 정의 코드가 만들어지고, 런타임 중 최초 loading시간에 1회 삽입 코드가 수행 됩니다. 태생 특성상 런타임 동작 시간을 조금 요구 하기는 하지만, 저장하는 값을 개발자가 할당 할 수 있는 장점이 있어 다른 목적으로도 응용이 가능 합니다.

옜날에 제가 한참 RTTI 기능이 필요했었을때에는 없던 프로젝트였는데, 2021년에 reflect-metadata를 활용한 typescript-rtti 프로젝트가 시작 되었더군요. 나름 그럴싸한 RTTI 기능들이 제공되는 package도 있어 공유 드려 봅니다.

5. NodeJS는 single threaded다?

NodeJS의 정의에 따라 답이 달라집니다. 정확하게 사용자 JS 코드를 한정 했을 시 single threaded가 맞습니다. 다만 내부적으로, 특히 IO와 관련한 engine 내부 동작들은 multi threaded로 동작 합니다. multi threaded로 구현이 되어 있는 비동기 처리들은, engine 내부의 thread pool을 통해 parallel로 처리가 되며, 먼저 완료되는 순서대로 사용자 code (JS, single threaded)로 event가 전달되는 구조 입니다.

특히나, 생각 외로 TCP/HTTP 구현이 잘되어 있어, 꽤 준수한 성능을 제공 합니다. 예전에 상당히 많은 수의 HTTP 호출을 시뮬레이션 했어야 했던적이 있었는데, C++, C#으로 작성된 코드보다 node로 작성된 코드가 훨씬 더 많은 요청을 처리 했었던 기억이 있습니다. (참고로 제 메이저 언어는 C++입니다)

node thread pool base class : https://github.com/nodejs/node/blob/master/src/node_internals.h : 253 line
NodeJS ThreadPool이 잘 정리 된 글 : node.js는 single thread일까?

2 Likes

혹여나 C# linq 좋아하시는 분들이라면, JS에서도 linq를 사용 하실 수 있습니다.

사실, array/object 대상으로 linq에 대응되는 기본 method들이 제공되어서 그다지 자주 쓸 일은 없긴 하지만요… 그냥 재미목적으로 구경해보시면 좋을 듯 합니다.