Embeddable-WG: WireGuard를 Node.JS로 관리하기 그리고 ChatGPT

Embeddable-WG: WireGuard를 Node.JS로 관리하기 그리고 ChatGPT
Photo by Jiawei Zhao / Unsplash

일반적인 JavaScript부터 TypeScript 그리고 JSX와 같이 많은 형태의 JavaScript를 보고 다룬 적은 있었지만 막상 C/C++ 코드를 Node.JS에서 사용할 수 있게 해주는 Node.JS 애드온을 다루어 본 적은 없었습니다. 그래서 WireGuard를 사용자 친화적으로 사용할 수 있게 해주는 앱을 만들고 싶다는 생각에 WireGuard를 애드온을 통해 관리하게 해주는 라이브러리를 만들고자 하였습니다.

GitHub - seia-soto/embeddable-wg: This package includes bindings of embeddable-wg-library in wireguard-tools library for efficient calls to set up WireGuard devices.
This package includes bindings of embeddable-wg-library in wireguard-tools library for efficient calls to set up WireGuard devices. - GitHub - seia-soto/embeddable-wg: This package includes binding...

애드온을 작성하면서 중요하게 생각했던 점은 무엇인가?

이 애드온을 작성할 때에는 단순히 JavaScript 뿐만 아니라 외부 라이브러리, TypeScript, C 소스코드까지 완성하려면 빌드해야 할 것이 산더미였습니다. 그리고 저는 본래 C 개발자가 아니기 때문에 최대한 프로젝트 자체를 단순하게 유지하는 것이 최적의 길이라고 생각합니다. C++를 사용했을 때 node-addon-api나 nan과 같은 라이브러리로 더 편하게 NAPI에 접근할 수 있지만 저의 책임을 온전히 다하기 위해서 돌아가더라도 C를 선택하였습니다.

또 프로젝트 특성 상 Linux 환경에서의 빌드 및 테스트가 요구되었는데 이 또한 프로젝트 구조를 간단하게 유지하는 것이 개발 환경 설정부터 유지까지 도움이 많이 됩니다. 그래서 단순히 node-gyp를 통해 C 소스코드를 빌드하는 것에서 시작했습니다.

C를 다루어 본 적이 없더라도 알 수 있는 것들.

gyp에게 무엇을 빌드할 지 알려주는 binding.gyp 파일 작성 이후 저는 C를 어떻게 작성해야 할 지 고민했습니다. 그래도 제가 C를 다루어 본 적이 없더라도 JavaScript를 다루면서 모든 개발 과정에서 공통적으로 찾을 수 있는 것들이 있었습니다. 그래서 C를 다루면서 최대한 경험을 살려보기로 했습니다.

  1. 보일러플레이트 코드 줄이기
  2. 오류 처리를 늦추거나 미루지 않기
  3. 일관성 있는 함수 패턴 사용하기
  4. 파일들을 효율적으로 배치하기

AI와 함께하는 코드 작성.

저는 이 과정에서 ChatGPT의 도움을 받기로 했습니다. C를 작성해나가면서 개별적인 것들은 모두 찾아서 제가 작성할 수 있었지만 그 외에 효율적인 대안이 있는지 찾는 것은 ChatGPT를 사용하는 것이 훨씬 효율적이었기 때문입니다. 특히 검색엔진에서는 정답을 하나로 응집하는 유형의 검색 결과를 많이 보여주기 때문에 불리한 요소로 작용하는 것을 타파하고 싶었습니다.

제가 C를 오랜시간 다루었다면 메뉴얼 페이지가 익숙할 것이고 AI보다 더 빠른 시간 안에 검색 결과를 머릿속으로 가져와 해결책을 생각했을겁니다. 하지만 그렇지 않았기 때문에 AI를 사용하여 제가 알고 싶었던 주제와 그 답을 이해하기 위한 내용까지 단번에 아는 것이 더 빨라 AI를 사용했습니다.

특히 AI의 답변을 보면서 C에서는 대략적으로 사람들이 함수를 사용하는 패턴을 익히고 활용하게 되었습니다. 다른 언어와 달리 언어적 단순함 때문에 함수의 성공과 실패 여부를 반환 코드로 사용하고 변경하고 싶은 변수가 있다면 포인터를 사용해서 함수에 넘겨준다는 사실을 간과하고 있었는데 답변 코드 중 일부에서 그 생각을 즉시 활용할 수 있었습니다.

Node.JS에서 받은 IPv4/IPv6 주소를 다시 sockaddr 등으로 바꿔야 할 때가 있었는데 Stack Overflow 등 검색을 통해 얻은 답변에는 getaddrinfo를 활용한 답변이 대다수였습니다. 그러나 AI에게서는 되려 더 간단한 방법이 있다는 말과 함께 inet_pton 함수를 활용한 코드를 받게 되었습니다.

if (inet_pton(AF_INET, __VA_ARGS__) != 1) {}
else if (inet_pton(AF_INET6, __VA_ARGS__) != 1) {}
else {} // AF_UNSEPC

단순히 이 코드를 받고 나서 활용 방법 이상으로 단순히 함수를 잘못 호출할 수 있는 가능성을 이용할 수 있었습니다. 특히 C와는 오류 처리 방식이 다른 JavaScript만을 계속 다루다보니 저는 계속 위와 같은 패턴을 피할 수 밖에 없었는데 고정 관념을 깨준 답변으로 기억됩니다.

AI가 비록 정답을 주진 않지만 C로 개발을 할 때의 뉘앙스를 빠르게 익히게 했습니다. 그 외 다른 것을 질문하면서 관습적이면서도 활용성이 좋은 팁을 많이 얻게 되었습니다. 아래는 그 중 일부입니다:

  1. C 매크로에서 한 번만 실행되는 do-while 문으로 매크로 내 정의되는 변수를 안전하게 사용할 수 있음.
  2. = {0} 문을 사용하는 것은 내부 필드 멤버에 포인터 변수를 NULL로 초기화 시킬 수 있음.
  3. #define 문으로 오류 코드를 정의해두는 것은 생각보다 흔하며 디버깅에 도움이 될 것임.
  4. strtok 외에도 string 버퍼 일부를 \0로 설정하여 문자열을 나눔.
  5. 메모리를 정리할 때 함수 내에서 goto 문을 사용할 수 있음.

C 프로젝트는 학교에서 시킬 때만 했던 제가 AI와 함께 코드의 뉘앙스를 알고 2일만에 꽤 익숙하게 C를 사용할 수 있었습니다. 그리고 아래 코드는 매크로를 한 번도 사용하지 않았던 제가 2일차에 완성한 코드입니다.

#define NAPI_CALL(env, call)                                                                                                      \
  if (call != napi_ok)                                                                                                            \
  {                                                                                                                               \
    const napi_extended_error_info *extended_error_info = NULL;                                                                   \
    napi_get_last_error_info((env), &extended_error_info);                                                                        \
    const char *error_message_str = extended_error_info->error_message ? extended_error_info->error_message : "No error message"; \
    if (extended_error_info->engine_reserved != 0)                                                                                \
    {                                                                                                                             \
      fprintf(stderr, "Error %s:%d: Call to %s failed with an error %s\n", __FILE__, __LINE__, #call, error_message_str);         \
    }                                                                                                                             \
    else if (strlen(error_message_str) > 0)                                                                                       \
    {                                                                                                                             \
      napi_throw_error((env), EWB_NNA_CALLFAIL, error_message_str);                                                               \
    }                                                                                                                             \
    else                                                                                                                          \
    {                                                                                                                             \
      napi_throw_error((env), EWB_NNA_CALLFAIL, "NAPI call failed");                                                              \
    }                                                                                                                             \
    return NULL;                                                                                                                  \
  }

#define ASSERT_NAPI_CALL(env, call, status)                      \
  if ((call) != napi_ok)                                         \
  {                                                              \
    napi_throw_error(env, EWB_NNA_CALLFAIL, "NAPI call failed"); \
    return status;                                               \
  }

AI가 비록 완벽한 코드를 짜준 적은 꽤 없습니다. Stable Diffusion에서 모두가 확인했듯이 글도 본떠서 작성합니다. 그래서 코드에서 함수의 사용은 엉망이었고 변수도 없는 변수를 만들어내기 바빴습니다. 하지만 빠르게 뉘앙스를 익힌 결과는 좋았죠. 저는 포인터 시스템에 익숙하지 않았기 때문에 처음에는 모든 코드를 하나에 함수에 넣기 일쑤였습니다. 그게 당시 저에게는 오류를 줄이는 길이었기 때문입니다. 그리고 질문을 받을 대상이 없었다면 과연 이렇게 2일이라는 시간 내에 빠르게 C를 작성할 수 없었을겁니다.

binding.gyp 오류 찾기.

개발의 많은 시간을 차지한 다른 부분은 Node-GYP 구성 요소인 binding.gyp의 오류 찾기였습니다. 단순히 빌드할 때 필요한 요소를 정리한 파일이지만 Python이 태생인 탓에 JSON에 오류가 있어도 찾을 수가 없었습니다. 단순히 포맷 오류라면 더 좋았겠지만 변수에 포함될 수 있는데 그 변수의 유무 등 검증할 수 있는 시스템이 마련되지 않았던 점이 발목을 잡았습니다.

KeyError: ‘binding.gyp:embeddable-wg#target’ · Issue #2805 · nodejs/node-gyp
Node Version: v18.10.0 (npm v8.19.2) Platform: Darwin aa-MacBook-Pro.local 22.3.0 Darwin Kernel Version 22.3.0: Mon Jan 30 20:39:35 PST 2023; root:xnu-8792.81.3~2/RELEASE_ARM64_T8103 arm64 Compiler...

오류는 한 줄이었고 이걸로는 단번에 원인을 찾을 수 없었습니다: KeyError: 'binding.gyp:embeddable-wg#target'

결국 node-pre-gyp에서 node-gyp를 호출할 때의 명령줄을 비교하며 module_name 변수가 원인인 점을 찾았습니다. 비슷한 오류를 보고 계시는 분은 명령줄에 대부분의 변수가 지정되니 참고하셨으면 좋겠습니다.


프로젝트 자체가 단순한 점과 C에 익숙하지 않았던 점을 제외하면 크게 적을 요소가 없네요. 새로운 유형의 프로젝트지만 단순함을 유지하면서 잘 끝낸 것 같아 아쉬운 점은 크게 없습니다. Glibc 뿐만 아니라 Musl libc와 ARM64 환경 또한 지원하니 관심이 있으신 분은 사용해보시면 좋을 것 같습니다.

Subscribe to Typed.sh

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe