티스토리 뷰

728x90

1. 문제 상황

NestJS로 서비스하고 있는 데 토스페이먼츠의 에스크로 등록 API는 EUC-KR 인코딩이 되어있다. 그로 인해 정상적으로 등록되지 못해 의도하지 않는 결과를 얻게 되었다.

이 문제를 파악하기 위해 공부했던 내용은 https://djunnni.tistory.com/10 에서 확인할 수 있습니다.

"정보 없음" 이 UTF-8에서 EUC-KR로 변환에 실패해 의도치 않는 결과가 나왔다

 

2. 기존 환경

NestJS 공식 문서에서 제공하고 있는 HttpModule을 이용해 외부 API를 호출하고 있다. 

 https://docs.nestjs.com/techniques/http-module

 

3. Axios의 application/x-www-form-urlencoded 처리 과정

httpModule은 내부적으로 axios를 사용하고 있다. Content-Type을 application/x-www-form-urlencoded로 설정하고 POST를 보내고 있다. 외부 라이브러리를 사용하고 있어 어떻게 요청을 처리하는 지 확인할 필요가 있다.

@nestjs/axios의 package.json을 확인하면 axios 1.5.0 버전을 사용하고 있다. axios의 github code로 dive 해보자.

 

README에서  제공하고 있는데 기본적으로 URLSearchParams를 활용하고 있으며 JavaScript Object를 JSON으로 직렬화해주고 있다. 

axios application/x-www-form-urlencoded 설명

 

이제 코드로 확실하게 확인해보자. axios/lib/defaults/index.js 를 확인해보면 transformRequest 함수가 존재하는데 요청에 대해서 데이터를 변환해주는 함수다.

axios/lib/default/index.js 일부

 

여기서 우리가 볼만한 정보는 3가지였다. 첫번째는 URLSearchParams를 사용하는 지 검증하는 부분과 isObjectPayload 여부를 확인하는 부분 그리고 Buffer 또는 File, Blob이면 그대로를 반환한다.

 

isObjectPayload는 toURLEncodedForm을 호출하는데 아래와 같은 함수로 구성되어 있다. 

toFormData는 data를 platform.classes.URLSearchParams으로 바꾸기 위해 visitor라는 option을 전달하여 생성하고 있다.
node환경에서 value가 버퍼면 base64로 추가하는 의도로 보인다.

axios url encoded form

 

3.1 URLSearchParams의 변환하는 방식

https://url.spec.whatwg.org/#interface-urlsearchparams 에서 class interface를 확인할 수 있다.

URLSearchParams는 URL의 query string을 만들어주는 utility method다. URLSearchParams은 Record 형태로 key-value 쌍으로 삽입이 가능하다. URL은 영어, 숫자, 기타 특수기호를 제외한 나머지는 %기호를 사용해 hex 코드로 변환된다.

 

따라서 URLSearchParams에 한글이 들어가면 hex코드로 변환된 형태로 output을 받는다고 이해하면 된다. 이 변환하는 과정에서 선행조건으로 배워둬야 할 것은 encodeURI와 encodeURIComponent 다.

 

3.1.1 encodeURI

아래 문자를 제외한 나머지를 전부 이스케이프 처리합니다.

A–Z a–z 0–9 - _ . ! ~ * ' ( )
; / ? : @ & = + $ , # -> URI 구문의 일부일 수 있음.

3.1.2 encodeURIComponent

아래 문자를 제외한 나머지를 전부 이스케이프 처리합니다. encodeURI와 달리 URI 구분자를 이스케이프하면서 value를 있는 그대로 서버로 보내줄 수 있습니다. 만약에 "lag&born"을 보내고 싶다면 encodeURIComponent를 사용해야 정확한 값을 보낼 수 있게 됩니다.

A–Z a–z 0–9 - _ . ! ~ * ' ( )

 

URLSearchParams에서는 key와 value 모두 encodeURIComponent로 인코딩되어 처리가 됩니다. 간단하게 코드를 테스트해보면 쉽게 알 수 있습니다.

const params = new URLSearchParams();
params.append("name&age", "daniel&25")

console.log(params.toString()) // name%26age=daniel%2625

 

정리해보면 axios는 application/x-www-form-urlencode에서 object 또는 urlSearchParams로 들어온 데이터를 urlSearchParams로 바꾼다. urlSearchParams는 encodeURIComponent를 사용해 key와 value를 인코딩한다. 인코딩은 UTF-8이 기본이다.

 

4. 어떻게 axios post를 호출해야 될까?

axios에서 urlSearchParams을 통해 UTF-8로 인코딩되는 건 좋지만 우리에게 필요한 건 EUC-KR입니다.

axios는 Buffer, Stream일 경우에는 그대로 data를 반환하도록 되어있습니다. 그 말은 body로 넣을 부분에 처음부터 EUC-KR로 들어간 Buffer를 전달하면 그대로 전달이 됩니다.

private async escrowAxios(url: string, obj: Data): Promise<string> {
    const { data } = await firstValueFrom(
      this.httpService
        .post<string>(url, converToBuffer(obj), { // convertToBuffer 목표: obj를 받아 euc-kr이 적용된 버퍼를 반환한다.
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
          },
          responseType: 'arraybuffer',
        })
        .pipe(
          catchError((error: any) => {
            return of({ data: error.message });
          }),
        ),
    );
    return iconv.decode(data, EUC_KR);
  }

이제 convertToBuffer를 구현하는 함수를 작성하면 완료가 됩니다.

 

5. convertToBuffer 목표

1. 일단 목표는 Buffer로 변하는 과정에서 Buffer에 들어있는 데이터는 EUC-KR을 통해 변환이 되어야 합니다.

2. body에 들어가는 데이터는 URLSearchParams의 결과 형태처럼 도출해야합니다.

key=value&key2=value2

3. 제공하는 API에서는 key에 들어가는 경우는 모두 영어이며 외부로부터 직접적인 위해를 가하지 못하기 때문에 value에 대해서만 검증을 하고 변환작업을 수행하면 됩니다.

 

5.1 코드 구현하기

/**
 * data를 받아 EUC-KR 문자열로 변환합니다.
 */
function convertToEucKrBuffer(obj: Data): string {
	let buffer = '';
	Object.entries(data).forEach(([key, value]) => {
      // iconv로 EUC-KR로 변환한 데이터를 HEX(16진수) String을 받아 2칸씩 자릅니다.
  	buffer += 
		`${key}=${appendPercentPer2Byte(
			iconv.encode(value, EUC_KR).toString('hex')
			)
		}&`;
	});

	return buffer.slice(0, -1);
}

/**
 *  HEX(16진수) String을 받아 2칸마다 % 기호를 추가합니다.
 */
function appendPercentPer2Byte(str: string): string {
	return [...value].reduce((acc, cur, idx) => {
		if (idx % 2 === 0) {
			return `${acc}%${cur}`;
        }
		return `${acc}${cur}`;
	}, '');
}

5.2 처리하면서

1. iconv-lite에서 encoding을 통해 출력을 해보면 Buffer에 잘 들어간다. 하지만 toString을 해보면 '%' 기호가 빠진다. 별도로 추가해줘야 한다.

2. JS에서 console.log 할 때, ? 표시가 뜨는 이유는 해당 인코딩을 지원하지 않아 발생하는 문제일 수 있다. 그럴 땐, decode 사이트를 이용해서 변환확인을 해봐야 한다.

 

6. 결론

서비스간에 사용하는 encoding을 확인하고 개발하는 습관을 항상 가지기

반응형