ABOUT ME

Email : haejin.yang91@gmail.com

Today
Yesterday
Total
  • express의 Request 타입 확장하기
    분석과탐구 2023. 3. 13. 11:57

    express의 Request 타입 확장이 필요한 상황

    // 익스프레스의 미들웨어
    function auth(req, res, next) {
    	// 인증을 다 한후에 id를 얻어낸다.
        // 이 id를 컨트롤러에서도 쓰고싶다.
        req.id = id;
    }
    
    app.get('/', auth, (req, res) => {
    	// 미들웨어에서 req.id에 id를 등록하여 가져올 수 있다.
    	const userId = req.id;
    });

    익스프레스에 미들웨어를 적용해서 사용하다 보면, 미들웨어에서 얻은 데이터를 컨트롤러로 그대로 넘기고 싶은 경우가 있다. 해당 데이터를 컨트롤러 쪽에서 다시 얻으려고 하면 미들웨어에서 했던 일을 반복해야 하는 경우가 있기 때문이다. 자바스크립트에서라면 `req.id = id;`처럼 그대로 넘겨도 무방하나, 타입스크립트에서는 자바스크립트처럼 바로 프로퍼티를 추가할 수 없다. 이를 해결할 방법이 바로 타입을 확장하는 것이다. 기존에 있는 타입에 새로운 타입을 추가하는 등의 일을 하는 것이다.

    방법1 declaration merging

    첫 번째 해결 방법은 전역적으로 Request의 타입을 확장하는 것이다. 사용된 타입스크립트 기법은 declaration merging이다. 네임스페이스를 합치고, 네임스페이스 안에 있는 인터페이스도 합친다. 이름이 같은 두 인터페이스를 하나로 합치면 두 인터페이스의 프로퍼티를 모두 포함하면서 이름은 같은 인터페이스가 된다.

     

    아래 해결법은 `in typescript, express Reqeust type extends...`정도로 검색을 하다 보면 어렵지 않게 구할 수 있는 해결법이다. 그런데, 굳이 왜 기록을 남기냐면, 타입스크립트, 익스프레스 버전에 따라 적용이 안된다는 기록을 많이 봐서 그렇다. 특히, 아래 기재한 내용 중에 `"typeRoots": ["./src/types", "./node_modules/@types"],`가 중요하다. 배열 안의 순서가 바뀌면 에러가 발생한다(왜????!).

    /*
    	파일: src/types/express/index.d.ts
    */
    declare namespace Express {
        export interface Request {
            id: string;
        }
    }
    
    /*
    	파일: src/middleware/auth.ts
    */
    const auth = (req: Request, res: Response, next: NextFunction) => {
        req.id = "myid";
        
        return next();
    }
    
    /*
    	파일: src/routes/users/auth.ts
    */
    const authRouter = Router();
    
    authRouter.get('/auth', auth, (req, res) => {
        console.log(req.id);
    });
    
    /*
    	파일: tsconfig.json
    */
    "typeRoots": ["./src/types", "./node_modules/@types"],
    
    /*
    	버전
    */
    "@types/express": "^4.17.17",
    "express": "^4.18.2",
    "typescript": "^4.9.5",

    방법2 인터페이스 상속

    두 번째 방법은 인터페이스 상속을 이용한 해결방법이다. 이 방법은 첫 번째 방법보다는 선호도가 낮은 것 같다. 하지만, 역시 어렵지 않게 찾을 수 있다. 그렇다면 왜 기록을 남기느냐? 여기서도 그냥 기록들을 따라가면 문제가 발생하기 때문이다. 어디서 문제가 발생할까? 바로 코드를 보기 전에 상속 관계에 있는 인터페이스를 다루는 코드를 보자.

    interface Base {
        id: string;
    }
    
    interface Derived extends Base {
        email: string;
    }
    
    let base: Base = {id: "myid"};
    let derived: Derived = {id: "mem", email: "abcd"};
    
    derived = base; // 에러 발생
    base = derived; // 컴파일 성공

    두 인터페이스를 따른 변수가 있다고 했을 때, 부모 인터페이스를 따르는 변수에는 자식 인터페이스를 따르는 변수를 대입할 수 있지만, 그 반대는 안된다. 자식 쪽에는 부모에 없는 프로퍼티가 있을 수 있기 때문이다. 하지만, 자식 프로퍼티가 옵셔널 하다면, 자식에 부모를 대입할 수 있다.

    interface Base {
        id: string;
    }
    
    interface Derived extends Base {
        email?: string;
    }
    
    let base: Base = {id: "myid"};
    let derived: Derived = {id: "mem", email: "abcd"};
    
    derived = base; // 통과
    base = derived; // 통과

    익스프레스의 Request를 상속하여 사용하는 경우도 동일하게 발생한다. 콜백으로 등록한 함수는 익스프레스 내부에서 다시 호출을 할 텐데, Request를 상속하여 만든 인터페이스로 정의한 콜백함수를 넘기면 자식(Request를 상속한 인터페이스)에 부모(Request)를 대입한 꼴이 되기 때문이다. 따라서 Request를 상속하여 사용하려면, 프로퍼티를 옵셔널로 지정해줘야 한다. 

    /*
    	파일: src/index.d.ts
    */
    import {Request} from "express";
    
    export interface IAuthRequest extends Request {
        id?: string;
    }
    
    /*
    	파일: src/middleware/auth.ts
    */
    
    const auth = (req: IAuthRequest, res: Response, next: NextFunction) => {
    	//인증...
        req.id = "myid";
    }
    
    /*
    	파일: src/routes/users/auth.ts
    */
    const authRouter = Router();
    authRouter.get('/auth', auth, (req: IAuthRequest, res) => {
    	console.log(req.id);
    }

    결론

    declaration merging은 타입이 적혀있는 원본 코드는 수정하지 않고 내 프로젝트에서 타입을 수정한다.

    인터페이스 확장은 원본 코드를 수정하지도 않고 타입도 수정하지 않는다. 일반적인 상속 관계를 이용하여 새로운 타입을 만들어낸다.

    '분석과탐구' 카테고리의 다른 글

    댓글

express의 Request 타입 확장이 필요한 상황


방법1 declaration merging


방법2 인터페이스 상속


결론


Designed by Tistory.