Node.js 굳이 브라우저용 언어를 서버에서 돌릴 필요가 있었을까
Cloudflare에 사이트 하나를 배포하려다 wrangler라는 도구를 만났고, wrangler를 이해하려다 npx를 만났고, npx를 이해하려다 결국 Node.js까지 거슬러 올라갔다. Node.js는 "JavaScript를 브라우저 밖에서도 실행할 수 있게 해주는 실행 환경"이라고 하는데, 여기서 문득 이상한 질문이 하나 떠올랐다. 서버 쪽 코드를 짤 수 있는 언어는 이미 많지 않았나? PHP도 있고, Python도 있고, Java도 있고. 그런데 왜 굳이 원래 브라우저 안에서만 쓰던 JavaScript를 서버에서까지 돌리려고 했을까.
언어가 없어서가 아니야
먼저 확실히 해둘 게 있다. Node.js가 등장한 건 2009년인데, 그전에도 서버 사이드 언어는 차고 넘쳤다. PHP, Perl, Python, Ruby, Java, C# 같은 언어들이 이미 수십 년째 서버 로직을 처리하고 있었다. 그러니까 "서버에서 코드를 실행할 방법이 없어서" JS를 끌어온 게 아니다. 진짜 이유는 다른 데 있었다.
문제는 "동시에 몇 명까지 감당할 수 있느냐"
전통적인 웹서버(예: Apache 같은 구조)는 대개 요청 하나가 들어올 때마다 스레드(혹은 프로세스)를 하나씩 배정하는 방식으로 동작했다. 이 방식 자체는 문제없다. 문제는 그 요청이 데이터베이스 조회, 파일 읽기, 외부 API 호출처럼 "응답을 기다려야 하는" 작업을 포함할 때 생긴다. 이런 작업을 처리하는 동안 해당 스레드는 그냥 멈춰서 기다린다. 이걸 blocking I/O라고 부른다.
동시 접속자가 몇 명뿐이면 아무 문제가 없다. 그런데 접속자가 수천, 수만 명으로 늘어나면 그만큼 스레드도 같이 늘려야 하고, 스레드 하나하나가 메모리를 차지하다 보니 서버가 감당하기 버거워진다. 이 상황을 업계에서는 한동안 "C10K 문제"라고 불렀다. 동시접속 만 명(Concurrent 10,000)을 어떻게 감당할 것인가 하는 문제였다.
JavaScript는 원래부터 "기다리지 않는" 언어
여기서 JavaScript가 뜻밖의 후보로 등장한다. JS는 원래 브라우저 안에서 이벤트 기반, 논블로킹 방식으로 동작하도록 설계돼 있었다. 브라우저에서 사용자가 버튼을 클릭하길 기다리는 동안 화면 전체가 멈추지 않고 다른 애니메이션이나 동작이 계속 처리되는 것처럼, "어떤 작업의 결과를 기다리는 동안 다른 일을 처리하다가, 결과가 준비되면 그때 콜백으로 처리한다"는 방식이 JS의 기본 동작 원리였다.
Node.js를 만든 라이언 달(Ryan Dahl)은 이 방식이 "요청 하나당 스레드 하나씩 붙잡고 기다리는" 전통적인 서버 모델보다, 동시 접속이 많고 I/O 대기가 잦은 애플리케이션에 훨씬 효율적이라고 봤다. 실시간 채팅, 알림, 스트리밍처럼 "많은 사람이 동시에 연결돼 있지만 각자는 대부분의 시간을 기다리며 보내는" 서비스에서 특히 그랬다. 하나의 프로세스가 스레드를 수만 개 띄우지 않고도, 수만 개의 동시 연결을 적은 메모리로 처리할 수 있었기 때문이다.
정리하면, "브라우저에서 쓰던 언어라서 굳이"가 아니라, "브라우저에서 이미 검증된 비동기 처리 방식이, 마침 서버가 새롭게 맞닥뜨린 대규모 동시접속 문제에 잘 맞았다"는 쪽에 가깝다.
왜 그 방식이 자원을 더 아낄 수 있었을까
의사는 몇 명 안 되는데 환자는 많아서, 접수하고 나면 진료를 받기까지 몇 시간씩 걸리는 병원이 있다고 하자. 대기실 좌석도 그렇게 넉넉하지 않다.
기존 서버 방식은 이런 병원과 같다. 접수를 마치면(=요청 처리를 시작하면), 이름이 불릴 때까지 대기실 의자에 계속 앉아 있어야 한다. 이유는 간단하다. 병원이 환자를 따로 찾아가거나 연락할 방법이 없기 때문에, 환자가 그 자리에 있어야만 이름이 불렸을 때 놓치지 않고 들어갈 수 있다. 그런데 대기실 의자 수는 한정돼 있다. 환자가 몰리는 시간대에는, 대부분의 환자가 실제로 진료를 받는 중이 아니라 그냥 순서를 기다리는 중인데도 의자를 다 차지해버려서, 새로 접수한 환자는 앉을 자리조차 없어 복도에 서서 기다려야 하는 상황이 벌어진다. 스레드 하나가 응답을 기다리는 동안 메모리를 붙잡고 있는 것도 똑같은 이치다.
Node가 택한 방식은 접수 절차 자체를 바꾸는 것에 가깝다. 접수를 마치면, 병원이 "앞으로 3명 남았습니다"처럼 카카오톡 알림을 보내주는 시스템을 쓴다. 환자는 그 알림만 믿고 병원 밖으로 나가 근처에서 볼일을 본다(=스레드가 다른 요청을 처리하러 간다). 순서가 다가오면 "지금 와주세요"라는 알림이 오고(=I/O 완료 이벤트), 그때 병원으로 돌아가 진료를 받는다. 이렇게 하면 대기실 의자는 실제로 진료 직전인 사람들만 잠깐씩 채우게 되고, 훨씬 많은 환자를 같은 병원, 같은 의자 수로 감당할 수 있게 된다.
그럼 병원은 왜 처음부터 후자로 안 했을까. 알림 시스템 자체가 저절로 생기는 게 아니기 때문이다. 환자별로 순서를 추적하고 알아서 메시지를 보내주는 시스템을 따로 만들어야 하는데, 이건 추가 비용과 수고가 드는 일이다. 환자가 하루에 몇 명 안 되던 시절에는 그냥 의자에 앉아 기다리게 하는 쪽이 훨씬 간단하고 저렴했다. 병원이 유명해지고 환자가 폭증해서 대기실이 미어터지기 시작하니까, 그제서야 알림 시스템을 만드는 게 낫다는 계산이 나온 것이다. 서버도 마찬가지였다. 동시접속이 적을 때는 스레드 하나가 요청 하나를 맡아 그 자리에서 기다리는 방식이 가장 단순하고 문제도 없었다. 동시접속이 수만 명 단위로 폭증하고 나서야, 번거롭더라도 콜백을 등록해두고 스레드는 다른 일을 하게 하는 이벤트 루프 방식이 그만한 값어치를 하게 됐다.
여기에 두 가지가 더 힘을 보탰다
하나는 순수한 속도 문제였다. 구글이 크롬 브라우저를 만들면서 V8이라는 JS 엔진을 극도로 빠르게 최적화해뒀는데, Node는 이 V8을 브라우저에서 떼어내 독립 실행 환경으로 만든 것이었다. 덕분에 실행 속도 면에서도 경쟁력이 생겼다.
다른 하나는 아주 실용적인 이유였다. 프론트엔드도 JS, 백엔드도 JS면 개발자가 언어를 두 개씩 익히거나 서로 다른 언어 사이를 왔다갔다 전환할 필요가 없어진다. 입력값 검증 로직 같은 코드를 클라이언트와 서버가 그대로 공유할 수도 있었다. 기술적 우아함과는 별개로, 이 편의성이 실제로 Node의 확산을 크게 가속시킨 요인이었다.
결론
서버 사이드 언어가 없어서 JS를 데려온 게 아니라, 동시 접속과 I/O 대기가 많은 환경에서 기존의 "스레드 하나당 요청 하나" 모델이 한계에 부딪혔고, 마침 JS가 그런 상황에 잘 맞는 비동기 처리 방식을 브라우저에서부터 이미 갖추고 있었다는 게 핵심이다. 여기에 V8의 속도와 "언어 통일"이라는 실용적 이점이 더해지면서, JS는 브라우저를 벗어나 서버까지 영역을 넓히게 됐다.