본문 바로가기

NCloud SDK ES6/TypeScript로 마이그레이션 하기

민이(MInE) 2024. 11. 3.
반응형

 

 

네이버 부스트캠프에서 클라우드 관련 프로젝트를 진행하기로 했는데, API를 사용하려고 SDK를 찾아보니 JavaScript ES5버전으로 작성되어 있어 코드를 한눈에 알아보기 힘들었습니다. 그래서 NCloud SDK를 현대적인 JavaScript 생태계에 맞춰 ES5에서 ES6/TypeScript로 마이그레이션한 과정을 설명해보려고 합니다. 

 

https://github.com/NaverCloudPlatform/ncloud-sdk-js

 

GitHub - NaverCloudPlatform/ncloud-sdk-js: Naver Cloud Platform Client Library for node

Naver Cloud Platform Client Library for node. Contribute to NaverCloudPlatform/ncloud-sdk-js development by creating an account on GitHub.

github.com

 

 

기술 스택

 

 

  • ES5 → ES6, TypeScript
  • CommonJS → ES Modules
  • Callback → Promise, Async/Await
  • superagent → axios

 

모듈 변경

 

우선 불필요한 의존성을 제거하고 TypeScript의 타입 정의를 활용하기 위해 모듈 정의 방식을 변경하였습니다.

 

  • ES5 모듈 구조
var url = require('url');
var hmacSHA256 = require('crypto-js/hmac-sha256');
var Base64 = require('crypto-js/enc-base64');
module.exports = signature;

 

 

  • ES6 모듈 구조
import { createHmac } from 'crypto';
import { ApiKeyCredentials, RequestConfig } from './types';
export function generateSignature;

//타입 정의
export interface ApiKeyCredentials {
    accessKey: string;
    secretKey: string;
}

export interface RequestConfig {
    method: string;
    url: string;
    timestamp: number;
    params?: Record<string, any>;
}

 

 

 

1. require -> import 구문으로 변경

2. crypto-js -> Node.js 내장 라이브러리 crypto 모듈로 변경

3. 타입 정의 추가

 

 

클래스 구조 변경

기존에 사용하던 프로토타입 기반 클래스 선언 방식을 ES6의 클래스 방식으로 변경했습니다.

  • 프로토타입 클래스
function ApiClient(apiKey) {
    this.basePath = apigwEndpoint + '/vserver/v2';
    this.apiKey = apiKey;
    this.authentications = {
        'x-ncp-iam': {type: 'apiKey', 'in': 'header', name: 'x-ncp-iam'},
    };
}

ApiClient.prototype.paramToString = function(param) {
    if (param == undefined || param == null) {
        return '';
    }
    return param.toString();
};

 

 

 

  • ES6 클래스
export class VpcApiClient {
    private readonly baseURL: string;
    private readonly apiKey?: ApiKeyCredentials;
    private readonly axiosInstance: AxiosInstance;

    constructor(apiKey?: ApiKeyCredentials) {
        this.baseURL = process.env.NCLOUD_API_GW || 'https://ncloud.apigw.ntruss.com';
        this.apiKey = apiKey;
        this.axiosInstance = axios.create({
            baseURL: this.baseURL,
            timeout: 60000,
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
        });
        this.setupRequestInterceptor();
    }

    private paramToString(param: any): string {
        if (param == null) {
            return '';
        }
        return String(param);
    }
}

 

 

1. 클래스 선언 방식 변경

  • 프로토타입 -> 클래스 기반

2. 멤버 변수 및 메소드 파라미터, 리턴값 타입 정의

3. 생성자 의존성 주입 패턴 적용

4. readonly 속성으로 불변성 확보

 

인증 처리 변경

중첩된 콜백 기반 인증 방식에서 promise 방식으로 비동기 처리 방식을 변경하였습니다.

 

  • 콜백 방식
exports.prototype.applyAuthToRequest = function(request, authNames, apiKey) {
    var _this = this;
    authNames.forEach(function(authName) {
        var auth = _this.authentications[authName];
        switch (auth.type) {
            case 'apiKey':
                if (apiKey) {
                    var timestamp = Date.now();
                    var data = {
                        'x-ncp-apigw-timestamp': timestamp,
                        'x-ncp-iam-access-key': apiKey.accessKey,
                        'x-ncp-apigw-signature-v1': signature(request, timestamp, apiKey.accessKey, apiKey.secretKey)
                    };
                    if (auth['in'] === 'header') {
                        request.set(data);
                    } else {
                        request.query(data);
                    }
                }
                break;
        }
    });
};

exports.prototype.callApi = function(path, httpMethod, pathParams,
    queryParams, headerParams, formParams, bodyParam, callback) {
    var url = this.buildUrl(path, pathParams);
    var request = superagent(httpMethod, url);
    
    if (this.timeout) {
        request.timeout(this.timeout);
    }
    
    request.query(queryParams);
    request.set(headerParams);
    
    if (formParams) {
        request.send(formParams);
    }
    
    var _this = this;
    request.end(function(error, response) {
        if (callback) {
            var data = null;
            if (!error) {
                try {
                    data = _this.deserialize(response, returnType);
                } catch (err) {
                    error = err;
                }
            }
            callback(error, data, response);
        }
    });
};

 

 

  • Promise 방식
private setupRequestInterceptor(): void {
    this.axiosInstance.interceptors.request.use((config) => {
        const timestamp = Date.now();
        const params = {
            ...config.params,
        };

        if (this.apiKey) {
            const signature = generateSignature({
                method: config.method?.toUpperCase() || 'GET',
                url: config?.url ?? '',
                timestamp,
                params,
                apiKey: this.apiKey,
                baseURL: this.baseURL,
            });

            if (config.headers instanceof AxiosHeaders) {
                config.headers.set('x-ncp-apigw-timestamp', timestamp);
                config.headers.set('x-ncp-iam-access-key', this.apiKey.accessKey);
                config.headers.set('x-ncp-apigw-signature-v2', signature);
            }
        }
        config.params = params;
        config.responseType = 'json';
        return config;
    });
}

async request(config: AxiosRequestConfig): Promise<any> {
    try {
        const response = await this.axiosInstance.request({
            ...config,
            params: {
                ...config.params,
                responseFormatType: 'json',
            },
        });
        return response.data;
    } catch (error) {
        if (axios.isAxiosError(error)) {
            console.error('API Error:', {
                status: error.response?.status,
                data: error.response?.data,
            });
            throw new Error(error.response?.data?.message || error.message);
        }
        throw error;
    }
}

 

1. Axios Interceptor를 이용해서 인증 로직 중앙화

2. 비동기 처리 방식을 콜백 -> Promise, async/await 로 변경

 

 

사용 예시

 

  • createVpc
import { ApiKeyCredentials } from './types';
import { VpcApi } from './VpcApi';
import * as process from 'node:process';
import dotenv from 'dotenv';

dotenv.config();

async function main() {
    if (!process.env.NCLOUD_ACCESS_KEY || !process.env.NCLOUD_SECRET_KEY) {
        throw new Error('API Key not found');
    }

    const apiKey: ApiKeyCredentials = {
        accessKey: process.env.NCLOUD_ACCESS_KEY,
        secretKey: process.env.NCLOUD_SECRET_KEY,
    };

    const vpcApi = new VpcApi(apiKey);

    try {
        const result = await vpcApi.createVpc({
            regionCode: 'KR',
            vpcName: `test-vpc-${Date.now()}`,
            ipv4CidrBlock: '10.0.0.0/16',
        });
        console.log('VPC Created:', result);
    } catch (error) {
        console.error('Failed to create VPC:', error);
    }
}

if (require.main === module) {
    main().catch(console.error);
}

 

해당 파일을 실행하면 NCloud 콘솔에서 아래처럼 VPC가 생성된 것을 볼 수 있습니다.

마무리

이러한 마이그레이션을 통해 코드의 품질, 유지보수성, 그리고 개발자 경험이 크게 향상시킬 수 있고, TypeScript와 현대적인 JavaScript 기능들을 활용하여 더 안정적이고 효율적인 SDK를 제공할 수 있습니다.

여기까지 주요 변경 사항 위주로 설명해보았습니다. 관련 코드는 아래 레포지토리에 ncloud-sdk 디렉토리에 푸시해두었으니 참고하시면됩니다.

 

 

https://github.com/Gdm0714/web37-cloud-canvas

 

GitHub - Gdm0714/web37-cloud-canvas

Contribute to Gdm0714/web37-cloud-canvas development by creating an account on GitHub.

github.com

 

반응형

댓글