홈시리즈멘토링

© 2026 정기창. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

☕후원하기소개JSON Formatter러닝 대기질개인정보처리방침이용약관

© 2026 정기창. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

☕후원하기
소개|JSON Formatter|러닝 대기질|개인정보처리방침|이용약관

공개된 정보만으로 웹사이트 기술스택 추론하기 — F12와 curl이면 충분합니다

정기창·2026년 6월 5일

최근에 어떤 사이트를 처음부터 파악해야 할 일이 있었습니다. 곧 손을 대야 하는데 안쪽 사정을 전혀 모르는, 그런 사이트였습니다. 코드를 받기 전이라 들여다볼 수 있는 것은 브라우저에 뜨는 화면과, 서버가 응답에 담아 보내주는 정보뿐이었습니다. 그 상태에서 "이 사이트는 무엇으로 만들어졌을까"를 가늠해보려 하니, 생각보다 많은 것이 이미 바깥으로 흘러나와 있다는 생각이 들었습니다.

이 글에서 제가 지킨 원칙은 하나입니다. 공개된 사이트에 정상적인 요청을 보내고, 서버가 스스로 응답에 담아 보내준 것만 읽는다. 로그인을 우회하거나, 비공개 경로를 들쑤시거나, 부하를 거는 일은 하지 않았습니다. 그 선 안에서, 브라우저 개발자도구(F12)와 curl만으로 한 사이트의 프론트엔드와 백엔드 기술스택을 어디까지 합리적으로 추론할 수 있는지를 정리해보려 합니다. 등장하는 사이트는 철저히 익명이고, 아래의 헤더와 코드 예시는 전부 설명을 위해 새로 지어낸 가상의 것들입니다.

시작하기 전에 — 두 개의 선을 먼저 긋습니다

방법으로 들어가기 전에, 두 가지를 먼저 분명히 해두고 싶습니다. 이 두 선을 그어두지 않으면, 멀쩡한 관찰이 어느새 선을 넘기도 하고, 데이터를 손에 쥐고도 엉뚱한 결론으로 미끄러지기도 합니다.

관찰과 침투를 가르는 선

첫 번째는 윤리의 선입니다. 공개된 사이트에 평범한 요청을 보내면, 서버는 응답 헤더와 HTML을 스스로 돌려줍니다. 그것을 읽는 일은 관찰입니다. 정상이고, 합법입니다. 반면 인증을 우회하거나, 공개되지 않은 관리자 경로를 추측해 두드리거나, 취약점을 찔러보거나, 일부러 부하를 거는 일은 침투입니다. 같은 호기심에서 출발해도, 이쪽은 선을 넘는 행위이고 불법의 소지가 있습니다.

저는 이것을 건물에 빗대어 생각합니다. 길에 서서 건물의 외관을 보고 "층수가 꽤 되네, 외장재는 유리겠구나" 하고 추측하는 것은 누구나 할 수 있는 일입니다. 하지만 잠긴 문을 따고 들어가 내부를 둘러보는 것은 전혀 다른 이야기입니다. 이 글에서 다루는 것은 처음부터 끝까지 밖에서 보이는 것에 한합니다.

웹서버·WAS·언어는 서로 다른 층입니다

두 번째는 개념의 선입니다. 헤더 하나를 보고 성급한 결론을 내리는 일을 막아주는, 작지만 중요한 구분입니다. 우리가 흔히 뭉뚱그려 "서버"라고 부르는 것은 사실 최소한 세 개의 층으로 나뉩니다.

  • 웹서버(nginx, Apache 등) — 요청을 가장 먼저 받는 문지기입니다. 정적 파일을 내려주거나, 뒤쪽으로 요청을 넘기는 리버스 프록시 역할을 합니다.
  • WAS(Tomcat, php-fpm, Node 프로세스 등) — 그 뒤에서 실제 애플리케이션 로직을 실행하는 층입니다.
  • 언어(Java, PHP, JavaScript 등) — 그 로직이 무엇으로 쓰였는지는 또 별개의 문제입니다.

이 세 층이 따로 논다는 사실을 놓치면, "응답의 Server 헤더가 nginx니까 백엔드도 Node겠지" 같은 결론으로 미끄러지기 쉽습니다. 그러나 nginx 뒤에는 Java로 짠 Tomcat이 앉아 있을 수도 있습니다. Server: nginx는 문지기가 nginx라는 사실만 알려줄 뿐, 그 안쪽의 언어에 대해서는 거의 아무것도 말해주지 않습니다. 외부에서 보이는 단서는 대개 이 가장 바깥 층에 묻어 있다는 점을, 처음부터 염두에 두는 편이 좋겠습니다.

프론트엔드 읽기 — 브라우저가 이미 다 보여주고 있습니다

프론트엔드는 비교적 정직합니다. 사용자의 브라우저에서 실행되어야 하는 코드이니, 어쩔 수 없이 상당 부분이 바깥으로 드러나기 때문입니다. 가장 먼저 보는 것은 프레임워크가 남기는 흔적입니다. 프레임워크마다 HTML이나 자바스크립트에 특유의 서명 같은 것을 남기는데, 이를 정리하면 다음과 같습니다.

프레임워크 / 도구 흔적의 예
React data-reactroot, __reactContainer 같은 속성·전역
Next.js __NEXT_DATA__ 스크립트, /_next/ 정적 경로
Vue v-if·v-for 디렉티브, data-v- 스코프 속성
Svelte svelte-1a2b3c 형태의 해시 클래스
번들러 webpack(webpackJsonp) / Vite(/@vite/, type="module")

라이브러리는 한결 더 노골적으로 드러나는 편입니다. 불러오는 파일의 이름이나 CDN 주소에 버전까지 박혀 있는 경우가 많기 때문입니다. jquery-3.7.1.min.js라는 파일명을 보면 jQuery의 버전을 그대로 알 수 있고, data-bs-toggle 같은 속성은 Bootstrap을, swiper나 gsap이 들어간 파일명은 각각의 라이브러리를 가리킵니다. 네트워크 탭에서 불러오는 리소스 목록을 한 번 훑는 것만으로도, 이 사이트가 무엇에 기대고 있는지 꽤 많은 그림이 그려집니다.

그다음으로 살펴볼 것은 렌더링 방식입니다. 같은 React라도 서버에서 내용을 채워 내려주는지(SSR/SSG), 빈 껍데기만 보내고 브라우저에서 그리는지(CSR)에 따라 작업의 성격이 달라지기 때문입니다. 이 둘을 구분하는 방법은 의외로 단순합니다. 브라우저의 '페이지 소스 보기'와 F12의 Elements 탭을 비교하는 것입니다. '페이지 소스 보기'는 자바스크립트가 실행되기 전의 원본 HTML을 보여주고, Elements 탭은 실행이 끝난 뒤의 결과를 보여줍니다.

서버가 내용을 채워 내려주는 사이트라면, '페이지 소스 보기'에 이미 본문이 가득 들어 있습니다. 반면 클라이언트에서 그리는 사이트라면, 원본 소스는 다음과 비슷하게 거의 비어 있고 본문은 Elements 탭에서만 나타납니다.

<!-- '페이지 소스 보기'에 본문이 없고 빈 컨테이너만 있다면 CSR 신호 -->
<body>
  <div id="root"></div>
  <script src="/static/js/main.4f2a1b.js"></script>
</body>

이 차이 하나만으로 "데이터를 서버에서 미리 박아주는가, 아니면 화면이 뜬 뒤에 따로 불러오는가"라는 큰 갈래를 가를 수 있습니다.

'next'라는 단어에 속을 뻔한 이야기

여기서 솔직하게 털어놓고 싶은 실수가 하나 있습니다. 이 사이트의 HTML을 받아 next라는 단어를 검색해보니 세 군데에서 잡혔습니다. 순간 "아, Next.js를 쓰는구나" 하는 생각이 스쳤습니다. 단어가 세 번이나 나왔으니 제법 그럴듯한 정황이라고 여겼던 것입니다.

그런데 하나씩 열어보니, 셋 다 이런 모양이었습니다.

<button class="swiper-button-next" aria-label="다음 슬라이드"></button>

전부 swiper-button-next, 즉 캐러셀에서 '다음' 슬라이드로 넘기는 화살표 버튼의 클래스명이었습니다. Next.js와는 글자만 겹칠 뿐 아무 관련이 없었습니다. 하마터면 캐러셀 화살표를 근거로 프레임워크를 단정할 뻔한 셈입니다. 부끄럽지만, 이 헛디딤에서 오히려 분명한 교훈 하나를 얻었습니다. 단어 하나가 잡혔다고 믿지 말고, 클래스명 전체와 경로, 그리고 그것이 놓인 맥락까지 보고 판단해야 한다는 것입니다.

이런 헛짚기는 사람만 하는 것이 아닙니다. 뒤에서 이야기할 자동 탐지 도구들도 이와 비슷한 방식으로 틀린 결론을 내놓곤 합니다. 단서를 모으는 일은 쉽지만, 그 단서가 진짜 무엇을 가리키는지 확인하는 일은 한 박자 더 느리게, 의심하며 해야 한다는 생각이 들었습니다.

백엔드 읽기 — 응답 헤더와 동작이 흘리는 단서

백엔드는 프론트엔드만큼 친절하지 않습니다. 로직이 서버 안에서만 돌기 때문입니다. 그래도 응답 헤더와 사이트의 동작에는 단서가 묻어 나옵니다. 먼저 curl로 헤더를 받아보겠습니다.

# 헤더만 받아보기
curl -I https://example.com

# 리다이렉트까지 따라가며 요청·응답 헤더 모두 보기
curl -sSL -D - https://example.com -o /dev/null

이렇게 받은 응답 헤더가 가령 다음과 같다고 해보겠습니다.

HTTP/2 200
server: nginx
x-powered-by: Express
set-cookie: connect.sid=s%3A...; Path=/; HttpOnly
content-type: application/json; charset=utf-8

여기서 Server: nginx는, 앞서 그어둔 선대로 문지기가 nginx라는 사실일 뿐 언어가 아닙니다. 정작 언어의 단서는 그 아래에 있습니다. X-Powered-By: Express는 Node 기반의 Express를 가리키고, 쿠키 이름 connect.sid 역시 Express 계열의 세션 흔적입니다. 두 단서가 같은 방향을 가리키니, 이 사이트의 백엔드는 Node일 가능성이 제법 높다고 볼 수 있습니다. 다만 X-Powered-By는 요즘 보안상 일부러 지우는 곳이 많아, 없다고 해서 이상한 일은 아닙니다.

쿠키 이름은 특히 정직한 단서인 경우가 많습니다. 세션을 쓰는 프레임워크가 기본값으로 박아두는 이름이 제각각이기 때문입니다.

쿠키 이름 가리키는 스택
JSESSIONID Java / 서블릿 컨테이너(Tomcat 등)
PHPSESSID PHP
connect.sid Express / Node
laravel_session Laravel(PHP)

URL의 생김새도 단서가 됩니다. 경로 끝에 .jsp나 .do, .action이 붙어 있으면 Java 진영을, .php면 PHP를, .aspx면 .NET을 떠올리게 됩니다. 물론 요즘은 확장자를 드러내지 않는 사이트가 많아 늘 통하는 단서는 아닙니다만, 보이면 꽤 강한 정황입니다.

헤더 너머, 사이트가 어떻게 동작하는지도 읽을거리입니다. 예를 들어 API 응답이 다음처럼 성공과 실패를 본문 안의 봉투(envelope)에 담아 보내는 경우가 있습니다.

{
  "result": "FAIL",
  "code": "E40010",
  "message": "요청을 처리할 수 없습니다",
  "data": null
}

흥미로운 점은 이때 HTTP 상태 코드가 200으로 내려오기도 한다는 것입니다. 실패인데 200을 주고, 실제 성패는 본문의 result 필드로 판단하게 하는 방식입니다. 이런 봉투 구조와 자체 에러코드 체계는 특정 프레임워크의 증거까지는 아니어도, 그 팀이 어떤 관행 위에서 API를 설계했는지를 짐작하게 해줍니다. 미정의 경로를 요청했을 때의 반응도 비슷한 결의 단서입니다. 깔끔하게 404를 주는지, 302로 홈에 돌려보내는 보수적인(fail-close) 처리를 하는지, 아니면 스택트레이스가 그대로 노출되는지에 따라 그림이 조금씩 달라집니다. 특히 스택트레이스가 새어 나온다면, 그것은 프레임워크를 가리키는 뜻밖의 단서가 되기도 합니다.

단서가 하나도 없다는 것도 정보입니다

그런데 어떤 사이트는, 헤더를 아무리 들여다봐도 단서가 보이지 않습니다. X-Powered-By도 없고, 세션 쿠키 이름도 평범하게 바뀌어 있고, URL에 확장자도 드러나지 않습니다. 처음에는 "아무것도 못 알아냈다"는 생각에 맥이 빠지기 쉽습니다.

그런데 가만히 보면, 이 '단서 없음' 자체가 하나의 정보입니다. 노출되기 쉬운 헤더들을 이렇게까지 의식적으로 지웠다는 것은, 그 사이트를 운영하는 쪽이 보안과 운영의 디테일을 꽤 신경 쓰고 있다는 신호이기 때문입니다. 그러니 이 경우의 결론은 "스택을 모르겠다"가 아니라, "기술스택을 잘 가렸고, 그만큼 운영이 성숙해 보인다"가 더 정확합니다. 비어 있음을 빈손이 아니라 하나의 관측으로 읽을 수 있게 되면, 보이지 않는 사이트 앞에서도 적을 말이 생깁니다.

추정과 확정을 구분합니다 — 정직한 '모르겠다'의 값

여기까지 단서를 모으고 나면, 그것을 어떻게 결론으로 옮길지가 남습니다. 저는 이 단계에서 단서마다 확신의 등급을 매겨두는 편이 좋다는 생각을 하게 되었습니다. 모든 단서를 같은 무게로 다루면, 강한 정황과 약한 짐작이 뒤섞여 어느새 전부 '사실'처럼 보이기 때문입니다. 등급은 대략 세 가지면 충분합니다.

등급 의미 예시
confirmed 직접 확인됨 jQuery 3.7.1 — 파일명 jquery-3.7.1.min.js로 확인
strong 강한 정황 백엔드 Node — X-Powered-By: Express + connect.sid
weak / 추정 약한 정황 백엔드 Java — 채용공고에 'Spring' 언급뿐, 헤더 단서는 0

이렇게 적어두면, 같은 보고서 안에서도 "이건 확인했다"와 "이건 그저 짐작이다"가 또렷하게 구분됩니다. 특히 마지막 줄처럼, 채용공고 같은 외부 정황만 있고 정작 응답에는 아무 흔적이 없는 경우를 weak로 묶어두는 것이 중요합니다. 외부에서의 관측은 결국 블랙박스를 더듬는 일이고, "아마 Java일 것"과 "Java다"는 전혀 다른 문장이기 때문입니다.

돌이켜 생각해보면, 이 작업에서 가장 프로페셔널한 태도는 화려한 단정이 아니라 정직한 보류였습니다. 틀린 확신을 자신 있게 적어두는 것보다, "헤더만으로는 백엔드 언어를 특정할 수 없습니다"라고 적는 편이 결과적으로 훨씬 믿을 만한 결론이 됩니다. 모르는 것을 모른다고 적는 일에는, 생각보다 큰 값이 있다는 생각이 들었습니다.

도구 정리와, 다시 윤리 이야기

지금까지의 과정에서 실제로 쓴 도구는 단출합니다. 특별한 장비는 필요하지 않았습니다.

  • 브라우저 개발자도구(F12) — Network 탭으로 불러오는 리소스를, Elements 탭으로 실행 후 DOM을, Sources 탭으로 스크립트 구조를 봅니다.
  • '페이지 소스 보기' — 자바스크립트 실행 전의 원본 HTML. 렌더링 방식을 가를 때 Elements 탭과 짝을 이룹니다.
  • curl -I / curl -sSL -D - — 응답 헤더를 날것 그대로 받아보는 가장 빠른 방법입니다.
  • Wappalyzer · BuiltWith — 기술스택을 자동으로 탐지해주는 확장입니다. 편리하지만, 앞의 swiper-button-next 같은 헛짚기를 똑같이 하므로 결과를 그대로 믿지는 않는 편이 좋습니다.

마지막으로, 이 글을 닫으며 처음의 선을 한 번 더 짚어두고 싶습니다. 여기서 한 일은 전부 관찰입니다. 서버가 스스로 내어준 것을 읽었을 뿐입니다. 인증을 우회하거나, 비공개 경로를 두드리거나, 부하를 거는 일—즉 침투는 호기심의 영역이 아니라 책임의 영역이고, 선을 넘는 순간 합법과 불법이 갈립니다. 궁금함이 거기까지 데려가려 할 때, 한 발 물러서는 감각을 갖는 것이 이 모든 방법론보다 먼저라고 생각합니다.

보안에 관해 한 가지만 덧붙이겠습니다. Server 헤더를 지우는 것은 흔히 security through obscurity(은닉을 통한 보안)라고 불립니다. 헤더가 드러나 있다는 사실 자체가 곧 취약점인 것은 아닙니다. 그저 공격자에게 1초짜리 힌트를 줄 뿐입니다. 진짜 보안은 헤더를 가리는 데 있지 않고, 제때 패치하고 설정을 조이고 인증을 단단히 하는 데 있습니다. 헤더를 가리는 일은 여러 겹의 방어 가운데 가장 얇은 한 겹일 뿐, 본질은 늘 그 안쪽에 있다는 생각이 들었습니다.

결국 이 작업에서 달라진 것은 도구가 아니라 태도에 가까웠습니다. 단정 대신 등급을 매기고, 침투 대신 관찰에 머무르고, 모르는 것은 정직하게 모른다고 적는 것. 한 사이트의 안쪽을 바깥에서 가늠하는 일은, 데이터를 모으는 기술이기 이전에 어디까지 말해도 되는지를 아는 절제의 문제였다는 생각이 듭니다.

기술스택HTTP헤더프레임워크프론트엔드백엔드개발자도구curl

관련 글

URL을 입력하면 무슨 일이 벌어지는가 — React, Next.js, NestJS, Docker 아키텍처 해부

React는 Node.js가 아니고, Next.js도 Node.js가 아닙니다. 하지만 Next.js는 Node.js 없이 SSR을 할 수 없습니다. 유저가 URL을 입력했을 때 React, Next.js, NestJS, Docker가 각각 어떤 역할을 하는지, 하나의 요청을 따라가며 정리했습니다.

관련도 89%

개발도 결국은 상품을 만드는 일 — 기술 스택 선택이 납품과 유지보수를 결정한다

사이드 프로젝트의 모던 스택을 클라이언트에게 납품하려 하니, 기술 선택이 곧 유지보수 비용과 클라이언트 경험을 좌우한다는 걸 깨달았습니다. 개발자가 시장과 목적에 맞는 기술 스택을 선택해야 하는 이유를 정리했습니다.

관련도 89%

쿠키를 훔쳐도 로그인이 안 되는 이유 — TLS 지문이 만드는 보이지 않는 방어벽

DevTools로 세션 쿠키를 복사해 curl에 붙이면 되는 사이트도 있고 안 되는 사이트도 있습니다. JA3/JA4 TLS 지문과 HTTP/2 SETTINGS 지문이 쿠키에 바인딩되는 원리를 정리했습니다.

관련도 88%