Frontend 19

Vite와 npm으로 React 프로젝트 시작하기: 시작점과 구성 요소 탐험

npm, Vite, dev server, package.json, main.jsx, App.jsx가 한꺼번에 생겨서 어느 파일이 시작점인지 모르겠다.

근거 · 교안 p96-p107

Vite와 npm으로 React 프로젝트 시작하기: 시작점과 구성 요소 탐험 대표 이미지

1장: Vite 프로젝트, 첫인상과 시작점

솔라의 눈은 검은 터미널 화면에 고정되어 있었다. 방금 실행한 npm create vite@latest 명령어의 결과로, 하얀 텍스트 몇 줄이 화면을 스크롤하며 새로운 파일들의 목록을 남겼다. 성공했다는 메시지가 떴지만, 솔라의 마음은 어쩐지 어수선했다.

my-react-app 폴더가 생성되었다. 그 안에는 package.json, index.html, 그리고 src라는 폴더가 있고, 그 폴더 안에는 또 main.jsxApp.jsx가 들어 있었다. 마치 누군가 조립 설명서도 없이 부품 한 상자를 눈앞에 쏟아 놓은 기분이었다. 이름들은 하나같이 낯이 익었지만, 이들이 모여 어떻게 하나의 웹사이트가 되는지, 그 시작점은 대체 어디인지 짐작할 수 없었다.

솔라는 혼잣말처럼 중얼거렸다. “분명히 앱의 시작은 App.jsx일 거야. 이름부터 ‘App’이잖아. 그런데 main.jsx는 뭐고, package.json은 또 뭐지? 다들 자기가 시작점이라고 주장하는 것 같아.”

그때, 방으로 들어온 루나가 솔라의 모니터를 흘끗 보더니 옅은 미소를 지었다. “새로운 프로젝트 키트를 받았네.”

“키트?” 솔라가 되물었다. “난 그냥 파일 더미로 보이는데. 어디서부터 손을 대야 할지 모르겠어.”

“네가 방금 실행한 명령어, 그게 그냥 파일을 복사해 온 게 아니야.” 루나는 터미널에 남아있는 명령어를 손가락으로 가리켰다. “그 명령어는 Vite라는 도구를 사용해서, React 앱을 바로 시작할 수 있는 기본 구조 전체를 ‘만들어준’ 거야. 어질러진 방이 아니라, 가구가 기본 배치된 새집에 가까워.”

‘가구가 배치된 새집’이라는 말에 솔라는 조금 다른 시선으로 파일 목록을 다시 보았다. 뒤죽박죽 섞인 부품 더미가 아니라, 각자의 자리에 놓인 물건들로 보이기 시작했다.

“그럼 이 파일들이 각자 어떤 역할을 하는지 알려면, 집 안을 둘러봐야겠네.” 솔라는 탐정처럼 눈을 빛내며 말했다.

루나는 고개를 끄덕였다. “좋은 생각이야. 일단 그 새집의 현관문, 즉 프로젝트 폴더를 열어볼까? 가장 먼저 뭘 확인해야 할까?”

솔라는 파일 탐색기를 열어 my-react-app 폴더로 들어갔다. node_modules, public, src 같은 폴더들과 index.html, package.json, vite.config.js 같은 파일들이 보였다. 여전히 많았다. “음… node_modules는 이름부터 무서우니까 일단 패스. src는 ‘소스’의 약자일 테니 중요한 코드가 들어있겠지. App.jsx가 저 안에 있었으니까.”

“맞아. 그럼 이 집의 설계도나 등기부등본 같은 건 어디 있을까?” 루나가 넌지시 물었다.

솔라의 시선이 package.json 파일에 멎었다. ”…이건가? json은 데이터 형식이고, package는 뭔가 꾸러미라는 뜻이니까. 프로젝트에 대한 정보가 담겨 있을 것 같아.”

솔라는 package.json 파일을 열었다. 복잡해 보이는 내용 속에서 익숙한 단어들이 눈에 띄었다. "name": "my-react-app", "version": "0.0.0". 그리고 조금 아래에 dependencies라는 항목이 있었다.

"dependencies": {
  "react": "^18.2.0",
  "react-dom": "^18.2.0"
},

“아! 여기 있네. 이 프로젝트가 reactreact-dom을 사용한다는 뜻이구나.” 솔라는 마치 숨겨진 단서를 찾은 것처럼 기뻐했다. “이 파일은 이 프로젝트가 어떤 기술들로 이루어져 있는지 알려주는 명세서 같은 거구나. 내가 생각했던 것처럼 그냥 파일 중 하나가 아니었어.”

“정확해.” 루나가 말했다. “Vite는 단순히 빈 폴더를 만드는 게 아니라, 이렇게 프로젝트의 정체성을 정의하는 package.json과 실제 앱의 내용을 담을 src 폴더 같은 핵심 구조를 미리 준비해주는 거야. 그래서 ‘Vite는 React 개발 서버와 기본 프로젝트 구조를 빠르게 만들어준다’고 말하는 거지.”

솔라는 고개를 끄덕이며 src 폴더를 열었다. 그 안에는 자신이 시작점이라고 추측했던 App.jsx와 함께, main.jsx가 나란히 있었다.

“이제 보니 App.jsx가 유일한 시작점은 아닐 수도 있겠어.” 솔라는 잠시 생각에 잠겼다. “Vite라는 목수가 집의 뼈대(src 폴더)를 세우고, package.json이라는 설계도를 남겨둔 거라면… 이 집의 전기를 켜고, 수도를 트는 스위치는 어디 있는 거지? 어떻게 이 파일들을 실행해서 웹사이트를 눈으로 볼 수 있는 거야?”

솔라의 시선은 다시 package.json 파일로 돌아갔다. dependencies 항목 위에 있던 scripts라는 부분을 뚫어져라 쳐다보았다. 거기에는 이런 내용이 적혀 있었다.

"scripts": {
  "dev": "vite",
  "build": "vite build",
  "preview": "vite preview"
},

“여기 dev라는 게 있네. 개발(development)의 줄임말인가? 터미널 안내문에서는 npm run dev를 실행하라고 했었는데… 이 스크립트가 프로젝트를 살아 움직이게 만드는 열쇠인 걸까?“

2장: package.json과 개발 서버의 연결고리

솔라는 package.json 파일의 scripts 부분을 뚫어져라 쳐다보고 있었다. ‘이 집의 전기를 켜는 스위치’는 분명 이 안에 있을 터였다. 프로젝트를 처음 생성했을 때 터미널에 나타났던 안내문이 머릿속을 스쳤다. npm install, 그리고 npm run dev.

솔라는 결심한 듯 터미널에 첫 번째 명령어를 타이핑했다.

npm install

엔터 키를 누르자, 화면이 수많은 텍스트로 빠르게 채워졌다. reactreact-dom을 포함해 package.json에 적혀 있던 의존성 파일들이 node_modules라는 폴더 안으로 차곡차곡 설치되는 과정이었다. 잠시 후, 분주하게 움직이던 커서가 멈췄다. 이제 두 번째 단계였다. 솔라는 침을 한번 삼키고 다음 명령어를 입력했다.

npm run dev

이번에는 결과가 달랐다. 몇 줄의 짧은 메시지와 함께, 터미널은 새로운 상태에 들어갔다. 초록색으로 빛나는 VITE라는 글자와 버전 번호가 보였고, 그 아래에는 Local:이라는 단어와 함께 웹 주소(http://localhost:5173)가 나타났다. 마치 기계의 시동이 걸리며 대기 상태에 들어간 듯, 커서는 더 이상 깜빡이지 않았다.

“와… 켜졌다!” 솔라는 작은 탄성을 질렀다. “정말로 npm run dev가 스위치였어.”

루나는 어느새 다가와 솔라의 모니터를 보고 있었다. “성공했네. 이제 그 주소로 들어가 보면 새집의 내부를 볼 수 있을 거야.”

솔라는 떨리는 손으로 브라우저에 해당 주소를 복사해 붙여넣었다. 화면에는 커다란 리액트 로고가 천천히 회전하고 있었고, 클릭할 수 있는 버튼도 보였다. 텅 비어 있던 파일들이 모여 하나의 살아있는 웹페이지가 된 순간이었다. 하지만 기쁨도 잠시, 솔라의 머릿속에는 새로운 의문이 피어올랐다.

“신기하긴 한데… 이상해. npm run dev라는 명령어가 어떻게 ‘개발 서버를 켠다’는 걸 아는 거지? npm은 ‘dev’라는 단어가 그런 뜻이라고 미리 약속이라도 해 둔 건가?” 솔라는 npm이라는 프로그램이 ‘dev’나 ‘start’, ‘build’ 같은 특별한 단어들을 알아듣고 정해진 행동을 하는 것이라고 막연히 추측했다.

루나는 솔라의 혼잣말을 듣고는 대답 대신, 솔라가 방금 전까지 보고 있던 코드 편집기 화면을 가리켰다. 화면에는 package.json 파일이 열려 있었다.

“네가 아까 찾았던 ‘설계도’에 다시 한번 가볼까? 네가 실행한 dev라는 스크립트는 그 설계도 어디에 있었지?”

솔라는 루나의 손가락을 따라 다시 package.jsonscripts 부분으로 시선을 옮겼다. 아까는 그저 ‘명령어 목록’이라고만 생각했던 부분이었다.

"scripts": {
  "dev": "vite",
  "build": "vite build",
  "preview": "vite preview"
},

“어…” 솔라는 잠시 말을 잇지 못했다. 자신의 추측이 빗나갔음을 깨달았다. dev라는 단어 옆에, 익숙한 이름이 적혀 있었다. "vite".

한순간에 머릿속에 흩어져 있던 조각들이 맞춰졌다. 터미널에 입력한 npm run dev 명령어. 그리고 package.json 파일 안의 "dev": "vite"라는 한 줄.

“아! npm run devnpm에 내장된 마법 같은 명령이 아니었어! 이건 그냥… npm한테 ‘이 프로젝트의 package.json 파일 안에 있는 scripts 목록에서 dev라는 이름표를 찾아. 그리고 그 이름표에 적힌 명령어(vite)를 실행해 줘’라고 시키는 거였구나!”

솔라의 목소리가 커졌다. “그러니까 이건 그냥 별명 같은 거네! vite라는 긴 명령어를 dev라는 짧은 별명으로 부를 수 있게 해주는 약속 장부인 거야. 이 package.json 파일이!”

“바로 그거야.” 루나가 조용히 고개를 끄덕였다. “그래서 다른 프로젝트에서는 npm run startnpm run serve 같은 명령어를 쓰기도 해. 중요한 건 명령어 자체가 아니라, 그 프로젝트의 package.json에 어떤 별명으로 어떤 실제 명령어가 등록되어 있는지 확인하는 거지.”

솔라는 이제 npm run dev 명령을 다른 눈으로 보게 되었다. 그것은 더 이상 정체를 알 수 없는 블랙박스가 아니었다. 내부 동작을 훤히 들여다볼 수 있는, 명확한 연결고리를 가진 시스템이었다. ‘Vite는 React 개발 서버와 기본 프로젝트 구조를 빠르게 만들어준다’는 말의 의미가 더 깊게 와닿았다. Vite는 단순히 파일 뼈대를 만들어주는 것을 넘어, vite라는 핵심 실행 명령어를 제공하고, package.jsonscripts를 통해 그 명령어를 npm run dev라는 표준적인 방식으로 쉽게 실행할 수 있도록 안내하는 역할까지 하고 있었던 것이다.

“이제 알겠어. package.json은 프로젝트의 신분증이기도 하고, 동시에 이런 명령어 단축키를 관리하는 리모컨 같은 거였네.” 솔라는 뿌듯한 표정으로 브라우저에 띄워진 리액트 로고를 바라보았다. 서버는 성공적으로 켜졌다. 그 방법도 이해했다.

하지만 시선이 다시 코드 편집기로 돌아가자 새로운 궁금증이 생겨났다.

“좋아, 이제 vite라는 엔진이 돌고 있다는 건 알겠어. 그런데 이 엔진은 어떻게 index.html, main.jsx, App.jsx 같은 여러 부품들을 조립해서 지금 내 눈앞에 보이는 이 화면을 만들어내는 거지? 전원은 켰는데, 전기가 어느 전선을 타고 흘러서 형광등(App.jsx)을 켜는 건지 아직 모르겠어.”

3장: main.jsx가 App.jsx를 화면에 띄우는 과정

개발 서버의 시동은 걸렸다. 솔라의 노트북 화면 한쪽에는 http://localhost:5173 주소가 열린 브라우저가, 다른 한쪽에는 코드 편집기가 나란히 놓여 있었다. 브라우저에서는 파란색 리액트 로고가 부드럽게 회전하고 있었고, 코드 편집기는 프로젝트 파일 목록을 보여주었다. 모든 것이 정상적으로 작동하는 것처럼 보였지만, 솔라의 마음속에는 풀리지 않은 매듭이 남아 있었다.

솔라는 App.jsx 파일을 클릭해 열었다. 파일 안에는 화면에 보이는 리액트 로고 이미지와 Vite + React라는 텍스트를 포함한 코드가 있었다. ‘이게 화면의 내용물인 건 확실한데…’ 솔라는 시험 삼아 <h1>Vite + React</h1> 부분을 <h1>안녕, 리액트!</h1>로 수정하고 파일을 저장했다. 놀랍게도, 저장하는 순간 브라우저 화면이 번쩍이며 텍스트가 즉시 ‘안녕, 리액트!’로 바뀌었다. 마법 같았다. 하지만 이 마법이 솔라의 혼란을 더 키웠다.

“분명히 App.jsx 파일이 화면을 그리고 있긴 한데… 대체 누가 이 App.jsx 파일을 실행하라고 시키는 거지?” 솔라는 혼잣말을 했다. “전원은 켰고(npm run dev), 형광등(App.jsx)도 켜졌어. 하지만 그 사이에 연결된 전선이 보이지 않아. vite라는 엔진은 어떻게 수많은 파일 중에서 하필 App.jsx의 내용을 브라우저에 보여주라고 아는 걸까?”

그때 조용히 지켜보던 루나가 말했다. “전선을 찾고 있구나. 그럼 가장 근본적인 질문부터 시작해볼까? 리액트가 없던 시절에, 웹 브라우저는 맨 처음 어떤 파일을 열어서 화면을 보여줬지?”

루나의 질문에 솔라는 잠시 생각했다. ”…HTML 파일. index.html 같은 거.”

“맞아. 우리 프로젝트에도 그 파일이 있지.”

솔라의 시선이 파일 탐색기에서 index.html을 찾아냈다. 프로젝트 최상위 폴더, src 폴더 바깥에 덩그러니 놓여 있었다. 솔라는 App.jsx가 주인공일 거라 생각했기에 이 파일은 무시하고 있었다. 그녀는 파일을 클릭해서 열었다.

<!doctype html>
<html lang="en">
  <head>
    <!-- ... meta tags ... -->
    <title>Vite + React</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

파일 내용은 놀라울 정도로 간단했다. <body> 태그 안에는 텅 빈 <div> 태그 하나와 <script> 태그 한 줄이 전부였다. 솔라의 눈이 그 두 줄에 고정되었다.

“어… 이건… body 안에 내용이 거의 없잖아. 그냥 idrootdiv 하나만 있고… 아!”

솔라는 무언가 발견한 듯 탄성을 질렀다. “바로 밑에 <script> 태그가 src/main.jsx 파일을 불러오고 있어! 그럼 브라우저가 가장 먼저 여는 건 index.html이고, 이 파일은 열리자마자 main.jsx를 실행시키는 거구나! 시작점은 App.jsx가 아니라 main.jsx였어!”

“절반은 맞았어.” 루나가 말했다. “그럼 이제 그 main.jsx 파일이 무슨 일을 하는지 확인해볼 차례네.”

솔라는 곧바로 src 폴더 안에 있는 main.jsx 파일을 열었다. 그 안에는 이런 코드가 있었다.

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

코드 조각들이 솔라의 머릿속에서 하나의 그림으로 맞춰지기 시작했다.

“잠깐만… import App from './App.jsx'… 여기서 App.jsx를 불러오고. 그리고… ReactDOM.createRoot(document.getElementById('root'))root라는 id를 가진 요소를 찾고 있어! 이건 아까 index.html에 있던 바로 그 div잖아!”

솔라의 목소리가 점점 커졌다. “그리고 그 요소에다가 .render(<App />)App 컴포넌트를 렌더링, 그러니까 그려 넣으라고 명령하고 있는 거야! 찾았다! 이게 바로 전선이었어!”

모든 연결고리가 드러났다.

  1. 브라우저는 index.html을 연다.
  2. index.html<body> 안의 <script> 태그를 통해 main.jsx를 실행한다.
  3. main.jsxReactDOM을 이용해, index.html에 있는 <div id="root"></div>라는 텅 빈 공간을 찾아낸다.
  4. main.jsx는 그 텅 빈 공간 안에 App.jsx에서 가져온 <App /> 컴포넌트를 그려 넣어 채운다.

App.jsx는 앱의 화려한 내용물이었지만, main.jsx는 그 내용물을 실제 세상(브라우저의 HTML 문서)과 연결하는 조립 설명서이자 실행기였던 것이다. ‘main.jsx가 App을 root에 렌더링한다’는 문장의 의미가 비로소 명확하게 다가왔다.

“이제 알겠어. 그럼 내가 만약 새로운 컴포넌트, 예를 들어 내 소개를 하는 Greeting 컴포넌트를 추가하고 싶으면… main.jsx를 건드릴 필요는 없겠네. main.jsx는 오직 App이라는 최상위 컴포넌트 하나만 연결해주고 있으니까. App 컴포넌트 안에 Greeting을 넣으면 되는 거구나.”

스스로 깨달음을 얻은 솔라는 망설이지 않고 행동에 옮겼다. src 폴더에 Greeting.jsx라는 새 파일을 만들고 간단한 코드를 작성했다.

// src/Greeting.jsx
function Greeting() {
  return <h2>안녕하세요, 리액트 탐험가 솔라입니다!</h2>;
}

export default Greeting;

그리고 다시 App.jsx 파일을 열어, 방금 만든 Greeting 컴포넌트를 가져와 가장 윗부분에 추가했다.

// src/App.jsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import './App.css'
import Greeting from './Greeting' // 새로 추가한 줄

function App() {
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <Greeting /> {/* 새로 추가한 줄 */}
      <div>
        {/* ... 기존 로고 이미지 ... */}
      </div>
      <h1>안녕, 리액트!</h1>
      {/* ... 기존 버튼과 텍스트 ... */}
    </div>
  )
}

export default App

파일을 저장하자마자, 브라우저 화면의 리액트 로고 위에 “안녕하세요, 리액트 탐험가 솔라입니다!”라는 문구가 선명하게 나타났다.

솔라는 자신의 코드가 만들어낸 변화를 뿌듯하게 바라보았다. 이제 그녀에게 Vite 프로젝트 구조는 더 이상 혼란스러운 파일 더미가 아니었다. index.html이라는 캔버스가 있고, main.jsx라는 붓이 있으며, App.jsx는 그 붓으로 그릴 메인 그림이라는 사실을 명확히 이해했다. 그리고 이제 그 그림에 새로운 요소를 더하는 방법까지 스스로 터득한 것이다.