본문 바로가기

[NCloud] NCloud SDK ES5에서 ES6+/TypeScript로의 마이그레이션

민이(MInE) 2025. 2. 13.
반응형

안녕하세요! 오늘은 네이버 부스트캠프에서 프로젝트를 진행하던 도중 NCloud SDK가 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

 

1. 인증 시스템 구조 개선

1.1. 타입 시스템 도입

TypeScript를 도입하면서 얻은 가장 큰 이점은 타입 안정성입니다. 예를 들어, 자격 증명 관련 인터페이스를 다음과 같이 정의했습니다

// types.ts
export interface ApiKeyCredentials {
    accessKey?: string;
    secretKey?: string;
    provider?: CredentialProvider;
    expiration?: string;
}

export interface CredentialProvider {
    readonly providerName: string;
    loadCredentials(): Promise<ApiKeyCredentials | null>;
}

 

 

1.2. 모듈 시스템 개선

기존의 ES5 코드는 UMD(Universal Module Definition) 패턴을 사용했습니다. UMD는 다양한 환경(브라우저, Node.js, AMD 등)에서 동작할 수 있도록 하는 패턴이지만, 코드가 복잡하고 가독성이 떨어진다는 단점이 있었습니다. 이러한 복잡한 패턴을 ES6의 모듈 시스템을 사용하여 간단하게 변경했습니다

// AS-IS (ES5)
(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        define(['superagent', 'querystring'], factory);
    } else if (typeof module === 'object' && module.exports) {
        module.exports = factory(require('superagent'), require('querystring'));
    } else {
        root.ServerRoleCredentials = factory(require('superagent'), require('querystring'));
    }
}(this, function(superagent, querystring) {
    // ...
}));

// TO-BE (ES6+)
import axios from 'axios';
import { ApiKeyCredentials, CredentialProvider } from '../types';

export class ServerRoleCredentials implements CredentialProvider {
    // ...
}

 

 

2. 자격 증명 제공자(Credential Provider) 현대화

2.1. 환경 변수 자격 증명 제공자

ES5의 프로토타입 기반 상속에서 클래스 기반 구조로 전환하면서 상속 구조의 이해도를 향상시키고, private/public 접근 제어의 명확성, 메서드와 속성의 관계 명확화 등의 많은 이점을 얻었습니다.

// EnvironmentCredentials.ts
export class EnvironmentCredentials implements CredentialProvider {
    private loaded = false;
    private credentials: ApiKeyCredentials | null = null;
    private readonly envPrefix = 'NCLOUD_';
    public readonly providerName = 'EnvironmentCredentials';

    async loadCredentials(): Promise<ApiKeyCredentials | null> {
        if (!this.needsToLoadCredentials()) {
            return this.credentials;
        }

        if (!process?.env) {
            throw new Error('No process info or environment variables available');
        }

        const accessKey =
            process.env[`${this.envPrefix}ACCESS_KEY_ID`] ||
            process.env[`${this.envPrefix}ACCESS_KEY`];

        if (!accessKey) {
            throw new Error(`Variable ${this.envPrefix}ACCESS_KEY_ID not set.`);
        }

        const secretKey =
            process.env[`${this.envPrefix}SECRET_ACCESS_KEY`] ||
            process.env[`${this.envPrefix}SECRET_KEY`];

        if (!secretKey) {
            throw new Error(`Variable ${this.envPrefix}SECRET_ACCESS_KEY not set.`);
        }

        this.loaded = true;
        this.credentials = {
            accessKey,
            secretKey,
            provider: this,
        };

        return this.credentials;
    }

    private needsToLoadCredentials(): boolean {
        return !this.loaded || !this.credentials;
    }
}

 

 

2.2. 설정 파일 자격 증명 제공자

// ConfigFileCredentials.ts
export class ConfigFileCredentials implements CredentialProvider {
    private loaded = false;
    private credentials: ApiKeyCredentials | null = null;
    public readonly providerName = 'ConfigFileCredentials';
    private readonly configureFilePath: string;

    constructor() {
        this.configureFilePath = path.join(
            os.homedir(),
            '.ncloud',
            'configure'
        );
    }

    async loadCredentials(): Promise<ApiKeyCredentials | null> {
        if (!fs.existsSync(this.configureFilePath)) {
            throw new Error(
                'Please check configure file (*inx : $HOME/.ncloud/configure , Windows : %HOME%₩.ncloud₩configure)'
            );
        }

        const config = this.parseConfigFile(
            fs.readFileSync(this.configureFilePath, 'utf-8')
        );

        if (!config.ncloud_access_key_id || !config.ncloud_secret_access_key) {
            throw new Error(
                `Failed to load credentials from the ${this.configureFilePath} file`
            );
        }

        this.loaded = true;
        this.credentials = {
            accessKey: config.ncloud_access_key_id,
            secretKey: config.ncloud_secret_access_key,
            provider: this,
        };

        return this.credentials;
    }
}

 

 

3. 인증 체인 시스템 개선

기존 ES5 코드에서는 콜백을 사용한 비동기 처리가 일반적이었습니다. 이를 async/await를 사용하여 다음과 같이 개선했습니다.

// CredentialProviderChain.ts
export class CredentialProviderChain {
    private readonly defaultCredentialsProvider: CredentialProvider[];

    constructor() {
        this.defaultCredentialsProvider = [
            new EnvironmentCredentials(),
            new ConfigFileCredentials(),
            new ServerRoleCredentials(),
        ];
    }

    async retrieveCredentials(): Promise<ApiKeyCredentials> {
        return this.retrieve(this.defaultCredentialsProvider);
    }

    private async retrieve(
        providers: CredentialProvider[]
    ): Promise<ApiKeyCredentials> {
        for (const provider of providers) {
            try {
                const creds = await provider.loadCredentials();
                if (creds) {
                    return creds;
                }
            } catch (error) {
                console.debug(
                    `Failed to load credentials from ${provider.providerName}:`,
                    error
                );
                continue;
            }
        }
        throw new Error('Unable to load credentials from any provider');
    }
}

 

4. API 클라이언트의 현대화

4.1. HTTP 클라이언트 교체

superagent에서 axios로 전환하여 더 나은 타입 지원과 현대적인 API를 제공합니다.

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();
    }
}

 

 

4.2. 서명 생성 로직 개선

export function generateSignature({
    method,
    url,
    timestamp,
    params,
    apiKey,
}: SignatureConfig): string {
    const space = ' ';
    const newLine = '\n';
    
    const fullUrl = params
        ? `${url}?${new URLSearchParams(params).toString()}`
        : url;

    const message = [
        method,
        space,
        fullUrl,
        newLine,
        timestamp,
        newLine,
        apiKey.accessKey,
    ].join('');

    return createHmac('sha256', apiKey.secretKey)
        .update(message)
        .digest('base64');
}

 

 

5. 주요 개선 사항

1. 타입 안정성 강화

  • TypeScript 도입으로 컴파일 시 타입 체크를 할 수 있게 했습니다.
  • 인터페이스를 통해 명확한 계약을 정의했습니다.
  • 더 나은 IDE를 지원합니다.

2. 비동기 처리 개선

  • 콜백에서 Promise/async-await로 전환했습니다.
  • 에러 처리를 강화했습니다.
  • 비동기 코드의 가독성을 향상시켰습니다.(기존 코드는 콜백 지옥으로 인해 코드를 해석하는 데 오래 걸렸습니다.)

3. 모듈화 및 구조화

  • ES6 모듈 시스템을 활용했습니다.
  • 관심사의 명확한 분리를 했습니다.
  • 재사용성을 향상시켰습니다.

 

ES5에서 ES6+/TypeScript로의 마이그레이션은 단순한 문법 변경 이상의 의미를 가집니다. 코드의 안정성, 유지보수성, 그리고 개발자 경험을 크게 개선할 수 있는 기회입니다. 하지만 이러한 마이그레이션은 신중하게 계획되고 단계적으로 진행되어야 합니다.

반응형

댓글