Hacker News

A Modern Web App in C

C로 만든 현대적인 웹 앱

저자는 OpenBSD, C, httpd, SQLite를 조합해 상용 웹 서비스를 만들고 CGI 방식으로 운영합니다. 단일 바이너리와 적은 의존성, 간단한 배포가 장점이지만 프로세스 생성 비용과 C의 메모리 안전성, 직접 구현해야 할 기능을 두고 논쟁이 이어집니다.

AI 요약

저자는 2026년에 상용 웹 제품 marketyour.games를 C와 CGI로 만들었습니다. 계정, 검색 대시보드, 폴더, Stripe 결제, Postmark 이메일을 갖춘 서비스이며 JavaScript 없이 점진적 향상(progressive enhancement)을 따릅니다. 저자가 고른 스택은 BCHS, 즉 OpenBSD, C, httpd, SQLite입니다. 프레임워크나 템플릿 엔진 없이 C 프로그램이 CGI 환경 변수를 읽고 HTML을 출력합니다. 별도의 데이터베이스 서버나 컨테이너도 두지 않습니다.

배포와 운영

요청이 들어올 때마다 새 프로세스를 실행하며, 배포는 바이너리 파일 하나를 교체하는 방식입니다. 기존 서버를 재시작하지 않아도 새 요청부터 새 바이너리가 처리합니다. OpenBSD의 pledge와 unveil로 C 프로그램의 권한을 제한하고, httpd는 chroot 환경에서 CGI 바이너리를 실행합니다. 저자는 libcurl과 libsodium을 포함한 라이브러리 세 개를 사용하며, 그중 빌드 의존성은 두 개라고 설명합니다. 두 라이브러리는 프로젝트 안에 vendoring해 빌드합니다. 깨끗한 상태에서 전체 빌드에 몇 초, 수정 뒤 재빌드에는 0.5초도 걸리지 않습니다.

정적 자산도 별도로 제공하지 않습니다. httpd의 모든 요청을 CGI 바이너리로 보내고, 자산을 실행 파일에 포함해 메모리에서 응답합니다. 바이너리는 정적으로 링크해 디렉터리에 놓습니다. 저자는 이렇게 구성 요소가 적으면 애플리케이션 전체를 파악하기 쉽다고 봅니다. 셸에서 프로그램을 직접 실행하고 요청을 입력해 응답을 확인하거나, stderr와 gdb로 디버깅할 수 있다는 점도 듭니다.

CGI를 다시 선택한 이유

저자는 보통의 웹 구성에서 nginx가 애플리케이션 서버 앞에 놓이는 방식을 두 개의 웹 서버가 겹친 구조로 봅니다. CGI에서는 httpd가 애플리케이션 코드를 직접 호출하므로 중간 서버 계층이 빠집니다. HTTP를 알면 CGI의 인터페이스도 이해할 수 있고, 1.1 명세가 2004년 이후 바뀌지 않아 배울 내용이 많지 않다는 주장입니다. 요청마다 프로세스를 새로 띄우는 방식은 서버리스와 닮았지만 특정 공급자에 묶이지 않고 예상 밖의 요금도 없다고 비유합니다.

의존성이 적다는 점도 선택 이유입니다. 저자는 여러 패키지가 간접 의존성을 수백 개씩 끌어오는 생태계와 달리, 자신의 빌드에는 C 컴파일러와 소수의 라이브러리만 필요하다고 설명합니다. 디버거, sanitizer, valgrind, ctags, cscope 같은 도구도 C 개발 환경에서 바로 쓸 수 있습니다. Vim에 ctags와 cscope 지원, gdb 프런트엔드가 포함된 점도 언급합니다.

프런트엔드와 데이터베이스

HTML과 CSS까지 C로 작성하는 일은 예상만큼 고통스럽지 않았다고 합니다. 프런트엔드와 백엔드가 한 언어에 있어 문법 강조나 플러그인 설정을 따로 조정할 필요가 없다는 설명입니다. 다만 C 문자열 처리가 번거롭다는 지적에 저자는 고정 크기 문자열을 다루는 자체 string view와 string builder를 작성했다고 답합니다. 유튜브 스크래퍼를 C로 만든 일은 HTML 출력보다 더 힘들었다고 덧붙입니다.

데이터베이스로는 PostgreSQL 대신 SQLite를 택했습니다. 별도 서버 프로세스나 설정, 서버 업그레이드가 필요 없고 애플리케이션이 파일을 직접 엽니다. 동시성에 관한 질문에는 WAL 모드에서 읽기는 동시에 처리되지만 쓰기는 한 번에 하나씩 진행된다고 답합니다. 쓰기가 막힐 때 즉시 시간 초과하지 않도록 busy timeout도 설정해야 합니다. 여러 프로세스가 SQLite 파일에 접근해도 쓰기 작업은 차례를 기다립니다.

직접 구현해야 하는 부분과 한계

CGI는 웹 서버와 애플리케이션을 잇는 인터페이스일 뿐, 프레임워크나 라이브러리가 아닙니다. 따라서 요청 처리, 쿠키, 인증, 출력 같은 기반 기능을 직접 만들거나 라이브러리를 골라야 합니다. 저자는 이를 cgi.h 하나에 구현했지만 최선의 선택인지는 확신하지 못한다고 밝힙니다. kcgi 같은 라이브러리를 쓰면 해당 API와 보안 모델을 받아들여야 하므로, stb 라이브러리처럼 필요한 유틸리티를 바로 가져다 쓰는 공용 묶음이 있으면 좋겠다고 제안합니다.

저자는 프로세스 생성 비용이 예전보다 줄었다고 보며, 성능 문제가 생기면 FastCGI로 옮길 수 있다고 설명합니다. 댓글에서는 요청마다 프로세스를 띄우는 데 드는 비용을 지적하며, 장기 실행 프로세스를 쓰는 FastCGI가 더 적합하다는 의견이 나옵니다. Go 역시 CGO 없이 단일 바이너리로 배포할 수 있고, C보다 동시성 처리와 메모리 안전성에서 낫다는 반론도 있습니다. C를 인터넷에 노출하는 일 자체가 위험하다는 우려도 제기됩니다.

Hacker News 반응

  • @fmo91 — 저자가 왜 C를 골랐는지 잘 모르겠습니다. 그래도 경험을 공유해 준 노력에는 감사드립니다.
  • @prophittcorey — C가 왜 다른 언어가 아니라 C인지 설명하려고 하지 않았습니다. 댓글이 언어 전쟁으로 번졌을 테니까요.
    • @kanbankaren — C 컴파일러를 만드는 사람으로서 C에 대한 집착이 이해되지 않습니다. 새로 시작하는 프로젝트라면 C를 원할 때 C++를 쓰세요. 일반 구조체만 쓰고 가상 함수를 피하면 실행 비용은 없고, 더 나은 정적 타입 검사와 lvalue 참조, 네임스페이스를 얻습니다.
    • @prophittcorey — 저는 보통 ‘C 스타일 C++’를 좋아하지만 C와 C++ 사이에 선을 긋기가 어렵습니다. ‘이 기능 하나만 더’라며 C++ 기능을 계속 쓰게 됩니다. 제 문제일 수도 있습니다.
  • @EGreg — 요청마다 새 프로세스를 실행하고 새 버전을 파일 교체만으로 배포한다는 부분이 FastCGI가 있는 이유라고 생각합니다. FastCGI에서는 프로세스를 오래 유지하면서 요청 사이에 상태를 정리합니다.
  • @Panzerschrek — 동시 요청이 들어올 때 어떻게 동작하나요? SQLite에서 여러 프로세스가 동시에 쓰기 작업을 할 수 있나요?
    • @prophittcorey — SQLite는 WAL 모드에서 여러 요청을 처리합니다. 읽기는 동시에 진행되지만 쓰기는 동기식입니다. 쓰기가 막혔을 때 바로 시간 초과하지 않도록 busy timeout도 설정해야 합니다.
    • @td521534f12 — SQLite는 동시에 하나의 쓰기 작업만 처리합니다. 여러 연결이 동시에 열려도 쓰기는 차례를 기다려야 합니다. WAL 모드에서는 애플리케이션이 로그에 내용을 추가하는 동안 단일 작성자가 WAL을 적용합니다.
  • @Scubabear68 — “C로 HTML과 CSS를 작성하는 일은 그리 나쁘지 않다”는 대목이 있습니다. C에서 문자열을 다루는 일은 전부 끔찍합니다.
    • @prophittcorey — HTML과 CSS를 출력하는 일은 그리 나쁘지 않았습니다. 유튜브 스크래퍼를 C로 만드는 일은 훨씬 더 고통스러웠습니다.
  • @devmor — 여기 적힌 이유 중 상당수 때문에 저도 PHP로 웹 애플리케이션을 만듭니다. 요즘 PHP는 괜찮습니다. 큰 팀이 작업할 프로젝트거나 복잡한 기능이 여럿 필요하지 않으면 프레임워크를 피합니다.
  • @ok123456 — Go 바이너리에서 CGO=0을 설정하고 템플릿과 문서를 포함하면 도구와 배포 경험이 더 낫습니다. scratch 컨테이너에 넣으면 완전히 격리할 수도 있습니다.
  • @rfgplk — 괜찮지만 절반짜리 접근입니다. 실제 웹 요청을 처리하려면 여전히 httpd에 의존하므로 성능과 표현 방식이 제한됩니다. 웹 애플리케이션과 웹 서버를 단일 바이너리로 묶는 편이 더 낫습니다. CGI는 요청마다 새 프로세스를 띄우는 오버헤드가 큽니다.
    • @sophacles — CGI에서도 임의의 HTTP 헤더를 보낼 수 있습니다.
  • @benlivengood — Go도 웹사이트나 서비스를 단일 바이너리로 제공할 수 있습니다. CGO_ENABLED=0을 쓰면 libc 연결도 피할 수 있고, nginx나 Apache 대신 쓸 HTTP와 TLS 라이브러리도 제공합니다. 저는 C를 좋아하고 과거에 C CGI도 작성했지만, C에서 동시성 프로그래밍을 좋아하지는 않습니다.
  • @UltraSane — C 코드를 인터넷에 연결하는 일은 좋지 않습니다. 우리는 그 사실을 계속 배워 왔습니다.
    • @assimpleaspossi — 지금 여러분의 서버, 운영체제, 데이터베이스가 모두 C로 작성됐을 수도 있습니다.
  • @jjuel — 실제로 어떻게 동작하는지 알 수 있도록 코드 예제가 있었으면 좋겠습니다.
    • @tomcam — BCHS 예제는 다음과 같습니다. pledge로 권한을 제한하고, Status: 200 OK와 Content-Type: text/html 헤더를 출력한 뒤 Hello, world!를 내보냅니다.
  • @chmaynard — 오픈소스가 아닌 것 같습니다. 제가 링크를 놓쳤나요?
  • @claiir — 저자는 플러그인 없는 Vim을 쓰고 AI도 쓰지 않는다고 했는데, AI가 쓴 블로그 글을 올렸네요.
  • @hollowonepl — 저도 C++로 현대적인 웹 API 서버를 직접 만들었습니다. 표준 라이브러리와 Boost만 썼는데, 안정적이고 의존성 문제도 적어 만족스럽습니다.
  • @macleginn — 여기서 근본적인 차이는 애플리케이션 바이너리가 항상 실행 중이지 않고 요청마다 시작된다는 점인가요? 제게는 아주 작은 차이처럼 보입니다.
    • @prophittcorey — 보통 구성에서는 nginx 뒤에 puma, unicorn, gunicorn 같은 서버가 있고 그 뒤에 애플리케이션 코드가 있습니다. CGI에서는 중간 서버가 빠져 nginx가 바로 애플리케이션을 호출합니다. 디버깅할 때 헤더가 어느 계층에서 바뀌거나 누락됐는지 따질 필요가 줄어듭니다.

원문: Hacker News / 번역·요약: Trawling