서비스 학습
SOURCE · fe/index.html · 2026-09-06

열두 줄짜리 빈 문서가
전체의 시작점이다

채팅 화면은 이 파일 어디에도 없다. 빈 div 하나와 script 태그 하나뿐이며, 나머지는 전부 JavaScript가 만들어 낸다.

이 파일을 만든 이유

브라우저는 주소를 받으면 가장 먼저 HTML 문서를 요청한다. React 애플리케이션이라도 예외가 없다. 그 첫 문서가 이 파일이며, 하는 일은 두 가지다. React가 붙을 자리를 하나 만들고, 애플리케이션 코드를 불러오는 것.

Vite에게도 특별한 파일이다. Vite는 이 index.htmlbuild의 진입점으로 삼아 <script>가 가리키는 파일부터 의존성을 따라간다. 그래서 위치가 src/가 아니라 프로젝트 최상단이다.

전체 구조에서의 위치

① 브라우저GET /이 파일을 받는다 — 화면은 아직 비어 있다
② script/src/main.tsxJS를 내려받아 실행
③ main.tsxcreateRoot(#root)빈 div를 React에 넘긴다
④ App.tsx화면 생성div 안이 채워진다

①과 ④ 사이가 빈 화면이다. 서버가 완성된 HTML을 주는 방식(SSR)과 달리, 화면을 만드는 책임이 전부 브라우저 쪽 JavaScript에 있다.

전체 코드

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>fe</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

한 줄씩 해석

<!doctype html>

브라우저에게 표준 모드로 해석하라고 알린다. 없으면 옛 호환 모드로 동작해 CSS 크기 계산이 달라진다. 첫 줄에 있어야 한다.

<html lang="en">

문서의 기본 언어다. 화면 글자는 대부분 한국어이므로 실제 내용과 어긋나 있다. Vite 기본 템플릿 값이 그대로 남은 것이다. 스크린 리더의 발음과 브라우저 번역 제안에 영향을 주므로 lang="ko"가 더 맞는다.

<meta charset="UTF-8" />

글자 인코딩 선언이다. 한국어를 다루므로 반드시 필요하고, head의 맨 앞에 있어야 한다. 브라우저는 이 선언 전까지 읽은 바이트를 다시 해석해야 하기 때문이다.

<link rel="icon" … href="/favicon.svg" />

탭에 표시할 아이콘이다. 경로가 /로 시작하므로 사이트 root 기준이며, 실제 파일은 fe/public/favicon.svg다. public/ 폴더의 파일은 build 때 변환 없이 dist/ 최상단으로 복사된다.

<meta name="viewport" …>

모바일에서 화면 폭을 실제 기기 폭으로 맞춘다. 이 줄이 없으면 iPhone·iPad Safari가 데스크톱 폭으로 그린 뒤 축소해 글자가 아주 작아진다. iPad에서 쓰겠다는 이 프로젝트의 목표에 직접 관계된다.

<title>fe</title>

탭 제목이다. 폴더 이름 그대로인 기본값이 남아 있다. 브라우저 탭·북마크·기록에 그대로 보이므로 서비스 이름으로 바꾸는 편이 낫다.

<div id="root"></div>

이 파일의 핵심이다. 비어 있는 것이 정상이다. main.tsxdocument.getElementById('root')가 이 요소를 찾는다. id를 바꾸면 그쪽도 함께 바꿔야 하며, 그렇지 않으면 흰 화면이 된다.

<script type="module" src="/src/main.tsx">

type="module"은 ES module로 다루라는 뜻이며, 이것이 있으면 브라우저가 HTML 파싱을 멈추지 않고 문서를 다 읽은 뒤 실행한다. 그래서 #root보다 뒤에 있어도, 앞에 있어도 동작한다.

.tsx를 직접 가리키는 점이 특이하다. 브라우저는 TSX를 실행하지 못한다. 개발 중에는 Vite가 요청을 가로채 변환해 주고, build 후에는 이 줄이 변환된 JS 파일을 가리키도록 바뀐다.

build 전후 비교

<div id=root></div>
<script type=module src=/src/main.tsx></script>
        ↑ Vite 개발 서버가 요청을 가로채 TSX 를 즉시 변환해 준다
<div id=root></div>
<script type=module crossorigin src=/assets/index-a1b2c3d4.js></script>
<link rel=stylesheet crossorigin href=/assets/index-e5f6g7h8.css>
        ↑ Vite 가 자동으로 바꿔 넣는다. CSS link 도 새로 추가된다

이 파일을 직접 고쳐 script 경로를 바꾸는 일은 없다. Vite가 build 때 대신 해 준다. 해시가 붙은 파일 이름 덕분에 Nginx가 자산을 1년 동안 캐시해도 안전하다.

실제로 확인하는 방법

확인방법보이는 것
원본은 비어 있다브라우저에서 view-source:로 열기div#root가 빈 채로 있고 채팅 UI가 없다
JS가 채운다개발자 도구 Elements 탭같은 div#root 안에 div.app-shell 이하가 들어 있다
build 결과npm run buildcat dist/index.htmlscript 경로가 해시 파일로 바뀌고 CSS link가 추가돼 있다
JS를 끄면브라우저 설정에서 JavaScript 차단완전히 빈 화면이 된다. SPA의 구조적 한계다

마지막 항목이 이 구조의 대가다. 검색 엔진과 JavaScript를 실행하지 않는 환경에서는 아무 내용도 볼 수 없다. 로그인 뒤에 쓰는 채팅 서비스라 지금은 문제가 되지 않는다.

자주 발생하는 오류

증상원인진단
흰 화면 + Cannot read properties of nullid="root"를 바꾸거나 지움main.tsxgetElementById 인수와 맞춘다
흰 화면, 콘솔도 조용함<script> 줄이 없거나 경로 오타Network 탭에 JS 요청 자체가 없다
한글이 깨짐charset 선언이 없거나 head 뒤쪽에 있음반드시 head 맨 앞이어야 한다
모바일에서 글자가 아주 작음viewport meta 누락iPad Safari에서 특히 두드러진다
build 후 404 자산base 설정과 배포 경로 불일치vite.config.tsbase를 확인한다
아이콘이 안 보임public/favicon.svg가 없음public/의 파일만 root로 복사된다

설계 선택과 대안

왜 HTML에 화면을 미리 그려 두지 않는가?

화면이 messages 같은 state에 따라 달라지기 때문이다. HTML에 초기 모습을 적어 두면 JavaScript가 그린 결과와 어긋나 깜빡임이 생기고, 두 곳을 함께 고쳐야 한다. React를 쓰는 이상 화면의 근거는 한 곳이어야 한다.

SSR로 첫 화면을 서버가 그리면?

빈 화면 시간이 줄고 검색 노출에도 유리하다. 대신 Node 서버가 하나 더 필요해지고, 현재 구조의 “Nginx가 정적 파일만 제공한다”는 단순함이 사라진다. 8GB 환경에서 container를 더 늘리는 선택이므로 지금은 하지 않는다.

langtitle은 지금 고쳐야 하나?

동작에는 영향이 없지만 둘 다 사용자에게 보이는 값이다. title은 탭에 그대로 fe라고 뜨고, lang은 보조 기술의 판단에 쓰인다. 인증 화면을 붙이는 8단계에서 서비스 이름이 정해지면 함께 정리할 만한 항목이다.

이전 단계와 다음 파일

이 파일이 불러오는 코드는 main.tsx이고, 그 코드가 그리는 화면은 App.tsx에 있다. 여기에 적용되는 전역 스타일은 index.css, build 규칙은 vite.config.ts, 배포 시 이 문서를 내보내는 설정은 nginx.conf에서 이어진다.

← 이전Frontend 구조다음 →React 진입점