라이브러리 타이핑하기
포스트
취소

라이브러리 타이핑하기

해당 포스트는 Node.js에 TypeScript 적용하기(feat. NodeBird) 강의로 typescript + nodejs의 내용을 복습하며 필요한 내용을 정리한 포스트입니다.




🌈 라이브러리 타이핑하기

💻 타입 없는 라이브러리 타이핑하기

본 강의에서는 프론트엔드는 다루지 않기 때문에 대체할만 것들을 직접 작성한다.

타입스크립트를 작성할 때 크게 두 가지 방법이 있다.

  1. 첫 번째는 리덕스처럼 처음부터 타입스크립트로 코드를 작성하는 방법
  2. JS로 먼저 코드 작성 후 타입스크립트를 붙이는 방식이다.

두 번째 방식으로 타입스크립트를 작성할 것이고, @types/passport-local 라이브러리를 직접 코딩하면서 테스트해볼 것이다.

그래서 기존에 설치했던 이 모듈은 삭제.

1
npm rm @types/passport-local

타이핑은 /back/types에서 할 것이다.

tsconfig의 typeRoots를 “./types”로 설정했기 때문이다.

/back/types 디렉토리에서 passport-local.d.ts 파일을 생성한다.

back/passport/local.ts의 코드를 보면서, 내가 사용하는 코드 부분에만 타이핑을 작성하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { Strategy as PassportStrategy } from 'passport';
import express = require('express');

declare module 'passport-local' {
  // delcare module은 실제 모듈의 이름과 같게 해야 에러나지 않음

  export interface IVerifyOptions {
    // export로 확장성 고려
    // 인터페이스 이름은 실제 모듈과 이름을 같게 만듦
    [key: string]: any;
  }

  export interface IStrategyOptions {
    usernameField: string;
    passwordField: string;
  }

  export interface Done {
    (error: Error | null, user?: any, options?: IVerifyOptions): void;
  }

  export interface VerifyFunction {
    (username: string, password: string, done: Done): void | Promise<any>;
  }
  // delcare module은 실제 모듈의 이름과 같게 해야 에러나지 않음
  export class Strategy extends PassportStrategy {
    constructor(options: IStrategyOptions, verify: VerifyFunction);
   }
}

다 작성을 했는데 local.tsdone(err) 부분에서 에러가 난다.

unkown은 'Error | Null' 에 할당될 수 없다는 문구가 뜬다.

그래서 catch 부분에서 err를 any로 타입을 정의해서 문제를 해결했지만, @types/passport-local을 참조해보니, done 부분의 매개변수들을 any로 설정한 것을 볼 수 있었다.

그래서 나도 any로 수정했다.

1
2
3
 export interface Done {
    (err: any, user?: any, options?: IVerifyOptions): void;
  }




💻 다양한 케이스를 위한 오버로딩

오버로딩? 같은 이름의 함수를 매개 변수에 따라 다르게 사용하는 것

local.ts 파일에 해당하는 타입 정의를 위해 직접 타이핑을 했었다. 그런데 만약 local 내의 코드를 수정하거나 확장해야 하는 경우라면 어떻게 해야 할까?

🔻 passport/local.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import passport from 'passport';
import bcrypt from 'bcrypt';
import { Strategy } from 'passport-local';
import User from '../models/user';

export default () => {
  passport.use(
    'local',
    new Strategy(
      {
        usernameField: 'userId',
        passwordField: 'password',
        session: false, // 세션에 로그인 정보 저장 안할 때 false
        passReqToCallback: true, // req 객체를 passport 인증 시 활용
      },
      async (userId, password, done) => {
        try {
          const user = await User.findOne({ where: { userId } });
          if (!user) {
            return done(null, false, { message: '존재하지 않는 사용자입니다!' });
          }
          const result = await bcrypt.compare(password, user.password);
          if (result) {
            return done(null, user);
          }
          return done(null, false, { message: '비밀번호가 틀립니다.' });
        } catch (err) {
          console.error(err);
          return done(err);
        }
      }
    )
  );
};

기존 코드에서 strategy 안에 session과 passReqToCallback 속성을 추가했다.

passReqToCallback 속성을 true로 설정하게 되면 req객체를 passport 인증시 활용해야 하기 때문에 콜백함수에 req 매개변수도 추가되어야 한다.

여기서 문제가 발생한다.

🔻 passport.local.d.ts

1
2
3
4
  export interface IStrategyOptions {
    usernameField: string;
    passwordField: string;
  }

우리가 사용하는 인터페이스에서는 추가되는 옵션들이 없기 때문에 추가해보면

1
2
3
4
5
6
  export interface IStrategyOptions {
    usernameField: string;
    passwordField: string;
    session?: boolean;
    passReqToCallback?: boolean;
  }

우리가 사용하는 인터페이스에서는 passReqToCallback 속성을 boolean로 하고 클래스 부분을 수정해봤다.

1
2
3
4
  export class Strategy extends PassportStrategy {
    constructor(options: IStrategyOptions | IStrategyOptionsWithRequest, verify: VerifyFunction | VerifyFunctionWithRequest)
    // | 연산자 사용하면 잘못된 결과가 나오거나 에러가 발생할 수 있음
  }

위의 경우 |를 통해 유니언타입으로 설정했는데 경우의 수가 다양해져 우리가 원하는 타입이 아닌 다른 결과가 나오거나 에러가 발생할 수 있다.

때문에 interface에서 boolean이 아닌 truefalse로 분리된 인터페이스를 설정해주는 편이 좋다.

속성의 옵션을 그때그때 다르게 사용하게 되는 경우 그때마다 작업해야 하는 요소가 생기게 되는 것이고, 이는 매우 비효율적인 방법이다. 이렇게 모듈들은 가변적인 매개변수를 받을 수 있고, 이런 다양한 케이스에 맞는 타이핑을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { Strategy as PassportStrategy } from 'passport';
import express = require('express');

declare module 'passport-local' {
  // delcare module은 실제 모듈의 이름과 같게 해야 에러나지 않음

  export interface IVerifyOptions {
    // export로 확장성 고려
    // 인터페이스 이름은 실제 모듈과 이름을 같게 만듦
    [key: string]: any;
  }

  export interface IStrategyOptions {
    usernameField: string;
    passwordField: string;
    session?: boolean;
    passReqToCallback?: false;
  }

  export interface Done {
    (err: any, user?: any, options?: IVerifyOptions): void;
  }

  interface IStrategyOptionsWithRequest {
    usernameField?: string;
    passwordField?: string;
    session?: boolean;
    passReqToCallback: true;
  }

  // req를 가장 앞 매개변수에 사용해야 하는 경우가 필요할 때, 오버로딩할 수 있는 인터페이스 생성
  interface VerifyFunctionWithRequest {
    (req: express.Request, username: string, password: string, done: Done): void | Promise<any>;
  }

  export interface VerifyFunction {
    (username: string, password: string, done: Done): void | Promise<any>;
  }
  // delcare module은 실제 모듈의 이름과 같게 해야 에러나지 않음
  export class Strategy extends PassportStrategy {
    // constructor(options: IStrategyOptions | IStrategyOptionsWithRequest, verify: VerifyFunction | VerifyFunctionWithRequest)
    // | 연산자 사용하면 잘못된 결과가 나오거나 에러가 발생할 수 있음
    constructor(options: IStrategyOptionsWithRequest, verify: VerifyFunctionWithRequest);
    constructor(options: IStrategyOptions, verify: VerifyFunction);
  }
}




📚 레퍼런스

Node.js에 TypeScript 적용하기(feat. NodeBird)

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.