[NCloud] NCloud SDK ES5에서 ES6+/TypeScript로의 마이그레이션
안녕하세요! 오늘은 네이버 부스트캠프에서 프로젝트를 진행하던 도중 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로의 마이그레이션은 단순한 문법 변경 이상의 의미를 가집니다. 코드의 안정성, 유지보수성, 그리고 개발자 경험을 크게 개선할 수 있는 기회입니다. 하지만 이러한 마이그레이션은 신중하게 계획되고 단계적으로 진행되어야 합니다.
댓글