EUC-KR 인코딩

시작

세상의 모든 웹사이트, api가 utf-8을 사용한다면 인코딩에 대해 전혀 신경쓸 필요가 없을 것이다. 물론 그런 일은 일어나지 않는다.

will-not-happend.jpg

그러므로 어느 시점에는 인코딩에 대해 신경써야될 일이 생긴다는 것이다.

현재 진행 중인 프로젝트에서 실명 인증 기능이 필요해서 구글링을 하다보니 나이스 평가정보에서 안심체크/실명확인이라는 서비스를 제공하고 있었다.

신청을 하고 며칠 뒤에 메일을 열어보니 암호화 모듈과 세 개의 매뉴얼을 보내주셨다. (JSP, ASP.NET, PHP)

개발 환경이 Node.js(Express)를 사용하기 때문에 잠깐 멘붕이 왔지만 다행히 PHP 매뉴얼을 참고하면 서비스 적용이 가능한 구조였다.

간략히 서비스의 구조를 설명하면 아래와 같다.

  • 암호화 모듈: OS별로 다른 모듈을 제공하는 듯하다. (처음에는 리눅스용 모듈을 제공받았고, 개발용 노트북이 맥이다보니 이후에 추가로 맥용 모듈을 제공받았다.)
  • 리턴코드: 암호화 모듈에 사용자 ID/PW와 함께 실명, 주민등록번호를 넘기면 리턴코드를 돌려준다. (2면 성공, 5면 실명/주민등록번호 불일치 등등)

충분히 예상가능하듯 /niceinfo 라는 써드파티 서비스를 감싸는 라우트를 만든뒤,

  1. Node.js의 exec을 활용해 암호화 모듈에 필요한 정보를 넘기고
  2. 실행 결과(리턴코드)에 따라 다른 응답을 돌려준다.

와 같이 굉장히 단순한 형태로 만들 수 있습니다.

이렇게 모든 일이 일사천리로 진행되고 아무런 어려움도 없었다면 제목이 삽질기록이 되지는 않았겠죠? 😢

방황

문제는 /niceinfo 라우트에 보내는 요청이 계속 같은 리턴코드만을 돌려준다는 것이었다.

리턴코드 관련 매뉴얼을 읽어보니 이는 사용자 ID/PW, 실명, 주민등록번호의 네 가지 인자 중 하나라도 누락되면 발생하는 에러코드였다.

물론 사용자 정보는 정확히 입력되어 있었고, 클라이언트로부터 실명과 주민등록번호도 잘 내려오고 있었다.

그렇게 서너시간 정도를 방황하다가 서비스 담당자분께 메일을 보냈다.

다음날 메일을 열어보니

  1. 꽤 여러번 요청을 보내셨다고 하셨는데, 해당 사용자 ID로 실제 저희 서버에 보낸 요청 중 확인 가능한 요청은 단 1건이다.
  2. 그리고 그 확인 가능한 요청의 경우 실명 인자에 영어를 넘겨줬다. (dfedede)
  3. 본인인증 서버의 경우 EUC-KR 인코딩을 사용하므로
  4. 두 가지 정보로 추측하건대 한글 이름의 인코딩 문제일 것으로 추측된다.

라는 메일이 와 있었다.

이제까지 인코딩에 대해 크게 신경써본 적이 없어서 EUC-KR이라는 인코딩이 있다는 것 정도밖에 몰랐기 때문에 더욱 당황스러웠다.

Iconv?

인코딩 변환 모듈을 찾다보니 iconv-lite라는 게 있었다.

곧바로 아래와 같은 코드를 작성했다.

function verifyRealName(name, juminNumber) {
    const encoded = iconv.encode(name, 'euc-kr');
    const decoded = iconv.decode(encoded, 'euc-kr');

    return exec(`${exePath} ... ${decoded}`);
}

위 함수의 동작은 우선 js 스트링(name)을 EUC-KR 버퍼로 인코딩한 뒤,
이를 다시 EUC-KR 인코딩으로 읽어들여 js 스트링으로 저장한다.
그리고 exec 함수에 인자로 넘겨준다.

예상으로는 decoded가 EUC-KR 인코딩을 사용한다고 생각할 수 있다.

그러나 실제로는 아무 일도 일어나지 않는다.

이는 decoded

  1. req.body로부터 utf-8로 인코딩된 내용을 EUC-KR로 읽어 버퍼에 저장한다.
  2. 우리가 원하는 인코딩 (EUC-KR)로 저장된 버퍼를 EUC-KR로 읽어 js 스트링에 저장한다.
  3. js 스트링은 내부적으로 UCS-2/UTF-16에 가까운 인코딩을 사용하여 맨처음의 값으로 돌아간다.

와 같은 과정을 거치기 때문이다.

해결

인코딩된 버퍼의 인코딩을 그대로 유지하면서 암호화 모듈에 인자로 보내려면 아래와 같이 해야한다.

  1. 인코딩된 버퍼를 임시 파일로 저장한다.
  2. cat 명령어를 통해 버퍼를 그대로 읽어서 넘긴다.

그러므로 위의 코드는

function verifyRealName(name, juminNumber) {
    const encoded = iconv.encode(name, 'euc-kr');

    // 인코딩된 버퍼를 임시저장할 경로
    const tempPath = path.resolve('app/temp/name');
    fs.writeFileSync(tempPath, encoded);

    // command substitution -> `` or $()
    // 명령어 실행 결과를 다른 명령어의 인자로 넘겨줌
    // 이 경우에는 cat이 파일에 담긴 바이트를 읽어서 그대로 전달
    return exec(`${exePath} ... \`cat ${tempPath}\``);
}

이렇게 해주고나서야 겨우 제대로된 리턴코드를 받아볼 수 있었다.

미래의 저를 위해 전체 코드 샘플도 같이 올립니다. 😄

import { exec } from 'child_process';
import path from 'path';
import util from 'util';
import iconv from 'iconv-lite';
import fs from 'fs';

import config from '../config';

async function saveEncodedName(name, filePath) {
  const encoded = iconv.encode(name, 'euc-kr');
  const promiseWriteFile = util.promisify(fs.writeFile);

  return promiseWriteFile(filePath, encoded);
}

async function verifyRealName({
  name,
  juminNumber,
}) {
  const {
    cbEncodePath, namePath, siteId, sitePW,
  } = config.niceinfo;

  const promiseExec = util.promisify(exec);
  const exectuablePath = path.resolve(cbEncodePath);
  const tempFilePath = path.resolve(namePath);

  /**
   * Niceinfo module accepts euc-kr encoded name only.
   * The only workaround to get things done is to get 'name' from file with euc-kr encoding
   * So read raw text from 'cat' command and pass it as argument for module
   * Ref:
   * How do I use the lines of a file as arguments of a command? (https://stackoverflow.com/questions/4227994/how-do-i-use-the-lines-of-a-file-as-arguments-of-a-command/4229151)
   */
  await saveEncodedName(name, tempFilePath);

  return promiseExec(
    `${exectuablePath} ${siteId} ${sitePW} ${juminNumber} \`cat ${tempNamePath}\``,
  )
    .then(({ stdout }) => stdout)
    .catch(err => String(err.code));
}

export default {
  verifyRealName,
};

교훈

  1. 리눅스 (명령어)를 이해하고 가깝게 지내자.
  2. body-parser와 같이 매일 사용하고 너무나 당연하게 여기는 모듈의 작동방식을 알아두자.
  3. 인코딩과 같은 주제에도 관심을 가지자.

가끔 어떤 버튼을 누르면 특정 위치로 이동하는 기능을 구현해야될 때가 있습니다. 이 경우 가장 쉽게 사용할 수 있는 방법은 html의 hash link (#)를 사용하는 것입니다.

Nuxt (Vue.js의 서버사이드 렌더링 프레임워크)의 라우팅은 vue-router를 통해 이뤄지는데, hash link (#id) 처리가 전통적인 html 페이지들과는 다릅니다.

전통적인 html 페이지에서 hash link는 몇 번을 누르더라도 언제나 해당 요소의 스크롤 위치로 이동합니다.

그러나 vue-router를 사용할 경우 scroll behavior에 hash link 처리를 해주더라도 처음 페이지를 로드할 때만 스크롤이 동작할 뿐, 두 번째로 링크를 누를 경우에는 아무런 반응이 없습니다.

이 문제를 해결할 유일한 방법은

  1. html 요소를 찾아 y 위치와 스크롤 y 위치를 더한뒤,
  2. 스크롤 함수에 계산된 y 위치를 넘겨주는 것입니다.

이 경우 아래와 같은 방식으로 코드를 작성할 수 있습니다.

goToSection(idx) {
    if (idx === 3) {
        setTimeout(() => {
            // sectionId를 가진 요소를 찾음
            const [target] = document.querySelector('#sectionId').getClientRects();
            const y = window.scrollY + target.top;

            // smooth scroll 옵션이 지원되는 브라우저에는 smooth scroll 옵션을 집어넣음.
            const scrollOptions = scrollHelpers.buildScrollOptions(0, y);
            window.scroll(scrollOptions);

            // vuetify 탭은 애니메이션을 사용하므로 약간의 딜레이를 줘서 동작을 자연스럽게 한다.
        }, 200);
    }
}

참고