첫 번째 React 앱 만들기
Vite 프로젝트의 파일 구조를 살펴보고 App 컴포넌트의 JSX와 CSS를 수정해 첫 React 화면을 완성합니다.
앞 글에서 Vite로 만든 프로젝트를 실행했다면, 이 글에서는 그 기본 틀 위에서 자신만의 첫 React 앱을 만들어 봅니다. 주로 src 폴더의 App.jsx 파일을 고치면서, React 코드가 실제로 어떻게 웹 페이지에 나타나는지 직접 확인합니다. 아직 모든 코드를 이해하지 못해도 괜찮습니다. 지금은 이 부분을 고치면 화면이 이렇게 바뀐다는 연결을 몸으로 익히는 것이 중요합니다.
앱 부트스트랩과 개발 피드백 — 상태 추적
실제 첫 root 연결과 App JSX 반환/commit 뒤 개발 모듈 갱신의 상태가 달라진다. Fast Refresh가 main을 매번 실행한다는 가정을 피하고 기존 DOM/화면 값을 추적할 수 있다.
React · UI와 실행의 관계
main.jsx는 React 루트를 만들고 App의 첫 렌더를 요청합니다
HTML DOM은 index.html의 #root 대상, React root는 main.jsx가 만든 런타임 루트입니다. App은 JSX를 반환하며 React가 DOM에 반영합니다. Vite의 개발 모듈 갱신과 첫 실행을 구분합니다.
실선은 전달·렌더 요청·DOM 반영, 점선은 JSX 반환과 개발 모듈 갱신입니다. 저장 시 지원되는 변경은 Fast Refresh로 반영하며 매번 main.jsx 재실행을 전제로 하지 않습니다. 모든 Vite 변경이 이 갱신만으로 끝난다는 뜻도 아닙니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 참여자 4명·메시지 8개의 앱 부트스트랩과 개발 갱신 순서다. HTML의 root 전달→React root의 App 렌더 요청→JSX 반환→HTML DOM commit, 이후 Vite 모듈 갱신→App 재렌더→JSX 반환→변경 commit이다. HTML은 DOM 대상을 가리키며 컴포넌트 계산은 React가 수행한다. 점선은 JSX 반환/개발 모듈 갱신이다. Fast Refresh가 매번 main.jsx를 재실행한다는 전제는 없다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 첫 root 연결 | root: DOM 대상 연결 / App: 첫 UI 계산 | main.jsx가 HTML #root 대상에 App 렌더를 요청한다 | HTML과 React의 계산 책임을 구분한다 |
| 첫 commit | UI: 첫 화면 / Vite: 개발 서버 연결 | App JSX 반환을 필요한 DOM에 반영한다 | JSX 반환과 DOM 반영은 별개다 |
| 지원되는 저장 갱신 | App: 변경 UI / main.jsx: 매번 재실행 전제 없음 | Vite 변경 모듈로 App을 다시 계산해 달라진 DOM을 commit한다 | Fast Refresh 지원 범위 밖은 전체 새로고침일 수 있다 |
앱 부트스트랩과 개발 피드백 — 상태 추적
실제 첫 root 연결과 App JSX 반환/commit 뒤 개발 모듈 갱신의 상태가 달라진다. Fast Refresh가 main을 매번 실행한다는 가정을 피하고 기존 DOM/화면 값을 추적할 수 있다.
React · UI와 실행의 관계
main.jsx는 React 루트를 만들고 App의 첫 렌더를 요청합니다
HTML DOM은 index.html의 #root 대상, React root는 main.jsx가 만든 런타임 루트입니다. App은 JSX를 반환하며 React가 DOM에 반영합니다. Vite의 개발 모듈 갱신과 첫 실행을 구분합니다.
실선은 전달·렌더 요청·DOM 반영, 점선은 JSX 반환과 개발 모듈 갱신입니다. 저장 시 지원되는 변경은 Fast Refresh로 반영하며 매번 main.jsx 재실행을 전제로 하지 않습니다. 모든 Vite 변경이 이 갱신만으로 끝난다는 뜻도 아닙니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 참여자 4명·메시지 8개의 앱 부트스트랩과 개발 갱신 순서다. HTML의 root 전달→React root의 App 렌더 요청→JSX 반환→HTML DOM commit, 이후 Vite 모듈 갱신→App 재렌더→JSX 반환→변경 commit이다. HTML은 DOM 대상을 가리키며 컴포넌트 계산은 React가 수행한다. 점선은 JSX 반환/개발 모듈 갱신이다. Fast Refresh가 매번 main.jsx를 재실행한다는 전제는 없다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 첫 root 연결 | root: DOM 대상 연결 / App: 첫 UI 계산 | main.jsx가 HTML #root 대상에 App 렌더를 요청한다 | HTML과 React의 계산 책임을 구분한다 |
| 첫 commit | UI: 첫 화면 / Vite: 개발 서버 연결 | App JSX 반환을 필요한 DOM에 반영한다 | JSX 반환과 DOM 반영은 별개다 |
| 지원되는 저장 갱신 | App: 변경 UI / main.jsx: 매번 재실행 전제 없음 | Vite 변경 모듈로 App을 다시 계산해 달라진 DOM을 commit한다 | Fast Refresh 지원 범위 밖은 전체 새로고침일 수 있다 |
위 그림은 HTML DOM, React 루트, App.jsx, Vite라는 네 참여자가 주고받는 여덟 개의 메시지로 앱이 처음 연결되는 순서와 개발 중의 갱신을 보여 줍니다. 처음에는 HTML의 #root 요소가 React 루트에 화면을 그릴 대상으로 전달되고, main.jsx로 시작한 React 루트가 App에 렌더를 요청합니다. App이 JSX를 돌려주면 React가 그 결과를 HTML DOM에 처음으로 반영(commit)합니다.
그 뒤 코드를 고쳐 저장하면 Vite가 개발 모듈을 갱신하고, App이 다시 렌더링되어 JSX를 돌려주며, React가 바뀐 부분만 DOM에 반영합니다. 그림에서 실선은 전달과 렌더 요청, DOM 반영을, 점선은 JSX의 반환과 개발 모듈의 갱신을 나타냅니다. 여기서 HTML DOM의 #root는 화면을 그릴 대상이고, React 루트는 main.jsx가 만든 런타임의 루트라서 둘은 같은 것이 아닙니다. 또 그림의 설명처럼 지원되는 변경은 Fast Refresh로 반영되며, 저장할 때마다 main.jsx를 다시 실행한다고 가정하지 않고, Vite의 모든 변경이 이 갱신만으로 끝나는 것도 아닙니다.
프로젝트 구조 살펴보기
VS Code로 my-first-react-app 프로젝트를 열고 왼쪽 탐색기(Explorer)를 보면 다음과 같은 폴더와 파일 구조를 볼 수 있습니다.
지금 npm create vite@latest가 받는 create-vite 9의 React 템플릿에서는 public 폴더에 vite.svg 대신 favicon.svg와 icons.svg가 있고, src/assets에는 react.svg와 함께 hero.png, vite.svg가 들어 있습니다. 이런 정적 파일의 이름은 템플릿이 바뀔 때마다 달라질 수 있으므로 위 구조와 달라도 괜찮습니다. 위 구조에서 지금 눈여겨볼 파일은 index.html, src/main.jsx, src/App.jsx 세 개입니다. index.html은 React 애플리케이션이 불러와질 기본 HTML 페이지로, 그 안에서 <div id="root"></div>라는 빈 요소를 찾아보세요. React 애플리케이션은 이 id="root"를 가진 div 안에 그려집니다.
src/main.jsx는 React 애플리케이션의 시작점입니다. App.jsx에서 가져온 <App /> 컴포넌트를 index.html의 root 요소에 연결하는데, 템플릿에서는 createRoot(document.getElementById('root'))로 React 루트를 만들고 render()로 App의 첫 렌더를 요청합니다. src/App.jsx는 지금 웹 페이지에 보이는 내용을 담은 메인 컴포넌트이며, 이 글에서는 주로 이 파일을 고쳐 첫 React 앱을 만듭니다.
정리하면 index.html 안의 모듈 script가 main.jsx를 불러오고, main.jsx가 App을 가져와 첫 렌더를 요청합니다. 파일 이름을 외우기보다 이 연결을 순서대로 따라가 보세요. 또 JSX에서 쓰는 함수 컴포넌트는 App처럼 대문자로 시작하며, 함수를 선언하는 것만으로는 브라우저 화면에 연결되지 않고 이렇게 루트에서 렌더해야 화면에 나타납니다.
App.jsx 수정하여 내용 변경하기
이제 VS Code에서 src/App.jsx 파일을 열어 보세요. 기본 템플릿에는 Vite와 React의 로고, 카운터 예제, HMR 안내 문구가 들어 있습니다. 이 예제 코드를 단순한 화면으로 바꾸면서, 컴포넌트 함수가 돌려준 JSX가 브라우저 화면이 된다는 점을 확인해 봅니다.
기본 예제에서 로고와 카운터에 관련된 import를 지우고, 다음처럼 간단한 <h1>과 <p> 요소를 돌려주도록 바꿉니다.
import './App.css';
function App() {
return (
<div className="App">
{/* 여기에 새로운 내용을 작성합니다 */}
<h1>나 혼자 React!</h1>
<p>첫 번째 React 앱을 만들고 있습니다.</p>
<p>정말 재미있네요!</p>
</div>
);
}
export default App;코드를 고친 뒤 Ctrl+S(macOS는 Cmd+S)를 눌러 파일을 저장합니다. 앞 글에서 npm run dev로 띄운 개발 서버가 아직 실행 중이라면, 터미널에 표시된 실제 Local 주소의 화면이 바뀌었는지 확인하세요. 화면에 “나 혼자 React!”, “첫 번째 React 앱을 만들고 있습니다.”, “정말 재미있네요!”라는 문구가 보이면 성공입니다. 바뀌지 않는다면 파일을 저장했는지, 개발 서버가 실행 중인지, 브라우저의 첫 오류가 무엇인지를 차례로 확인합니다.
JSX에 대하여 잠시 알아보기
App.jsx의 return 안에 쓴 <div className="App"> ... </div> 같은 코드는 HTML처럼 생겼지만, 사실은 JSX(JavaScript XML)라고 부르는 자바스크립트의 문법 확장으로, React 컴포넌트에서 자주 씁니다. JSX를 쓰면 자바스크립트 안에서 HTML과 비슷한 문법으로 UI를 기술할 수 있습니다. 겉보기에는 HTML과 매우 비슷하지만 몇 가지 중요한 차이가 있습니다.
첫째는 속성의 이름입니다. 이 React DOM 예제에서는 HTML의 class와 for에 해당하는 속성을 className과 htmlFor처럼 정해진 프로퍼티 이름으로 씁니다. 많은 이름이 onClick처럼 camelCase이지만, aria-*와 data-*는 하이픈 표기를 그대로 유지합니다. 그러니 모든 속성을 하나의 이름 변환 규칙으로 짐작하지 말고, HTML의 class·for와 JSX의 표기를 구별해 두세요.
둘째는 하나의 루트 요소입니다. 여러 JSX 태그를 나란히 하나의 표현식으로 돌려줄 때는 그 태그들을 하나로 묶어야 합니다. 예를 들어 두 태그를 나란히 돌려주려면 하나의 <div>나 <>(프래그먼트)로 감쌉니다.
// ⭕ 올바른 JSX (하나의 div로 감싸져 있음)
function MyComponent() {
return (
<div>
<h1>제목</h1>
<p>내용</p>
</div>
);
}
// ❌ 잘못된 JSX (두 개의 최상위 요소)
// function MyComponent() {
// return (
// <h1>제목</h1>
// <p>내용</p>
// );
// }잘못된 예가 실패하는 이유는 return 뒤의 JSX가 하나의 값이어야 하기 때문입니다. JSX 태그 하나는 함수 호출 하나로 바뀌므로, 두 태그를 나란히 적으면 함수가 값 두 개를 한꺼번에 돌려주려는 꼴이 되어 컴파일 오류가 납니다. <div>로 감싸기가 번거롭거나 불필요한 DOM 노드를 만들고 싶지 않을 때는 <></> 같은 빈 태그를 쓸 수 있는데, 이를 프래그먼트(Fragment)라고 부릅니다. 프래그먼트는 자식들을 하나로 묶기만 하고 실제 DOM에는 아무 요소도 남기지 않습니다.
// ⭕ 올바른 JSX (프래그먼트 사용)
function MyComponent() {
return (
<>
<h1>제목</h1>
<p>내용</p>
</>
);
}셋째는 자바스크립트 표현식의 삽입입니다. JSX 안에서 중괄호 {}를 쓰면 자바스크립트 표현식을 넣을 수 있어서, 변수의 값이나 함수의 결과를 UI에 바로 렌더링할 수 있습니다. 아래 컴포넌트는 name의 값을 넣어 화면에 “안녕하세요, 독자 여러분!”을 보여 줍니다.
function Welcome() {
const name = '독자 여러분';
return (
<h1>안녕하세요, {name}!</h1>
);
}이처럼 JSX는 자바스크립트 안에서 UI의 구조를 선언하게 해 주며, 이후의 예제는 모두 JSX로 React 컴포넌트를 작성합니다.
스타일 적용하기
지금의 웹 페이지는 글씨가 작고 정렬되어 있지 않아 보기에 좋지 않을 수 있습니다. React 컴포넌트에 스타일을 입히는 방법은 여러 가지가 있지만, 여기서는 Vite 기본 프로젝트에서 바로 쓸 수 있는 일반 CSS 파일인 App.css를 고쳐 봅니다. VS Code에서 src/App.css를 열어 기존 내용을 모두 지우고 다음 스타일을 넣은 뒤 저장합니다.
.App {
text-align: center; /* 텍스트를 가운데 정렬 */
background-color: #f0f0f0; /* 배경색 변경 */
min-height: 100vh; /* 최소 높이를 뷰포트 높이로 설정 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin); /* 폰트 크기 조정 */
color: #333; /* 텍스트 색상 변경 */
}
h1 {
color: #007bff; /* 제목 색상 파란색 */
margin-bottom: 20px;
}
p {
color: #555; /* 단락 텍스트 색상 회색 */
line-height: 1.5;
}브라우저를 보면 텍스트가 가운데로 정렬되고 배경색과 글자색이 바뀐 것을 볼 수 있습니다. App.jsx의 import './App.css';가 이 CSS 파일을 불러오므로, 그 스타일이 App 컴포넌트의 화면에 적용됩니다. 이 Vite 프로젝트에서는 Vite가 CSS 파일의 import를 처리하므로, 컴포넌트의 화면 구조와 스타일의 연결을 자바스크립트 파일에서 함께 따라갈 수 있습니다.
다만 기본 템플릿의 index.css에는 :root의 글꼴과 글자색, 배경색, 그리고 #root의 폭(width: 1126px, max-width: 100%)과 가운데 정렬, 좌우 테두리 같은 전역 규칙이 들어 있으므로, App.css만 바꾸면 화면 전체가 같은 모양이 되리라고 가정하지 않습니다. 이번 실습에서는 main.jsx의 import './index.css'는 그대로 두고, index.css의 내용을 다음처럼 바꿔 전역 조건을 고정합니다. App.css는 앞의 코드를 그대로 씁니다.
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 0;
font-family: system-ui, sans-serif;
}
#root {
width: 100%;
min-height: 100vh;
}
.App {
padding: 16px;
}body의 기본 바깥 여백(margin)을 없애고 모든 요소의 box-sizing을 border-box로 바꾸었으므로, App.css의 min-height: 100vh는 index.css가 더한 16px의 안쪽 여백까지 포함해 App이 화면 높이를 정확히 채웁니다. border-box가 아니었다면 위아래 여백 32px가 더해져 세로 스크롤이 생겼을 것입니다. body의 min-width: 0은 일반 블록 요소에서 초기값 auto가 계산되는 값(0)과 같아 화면을 바꾸지 않으며, 좁은 화면에서 긴 제목이 줄을 바꾸는 것은 텍스트의 기본 줄바꿈 때문입니다. 글자색과 배경색은 App.css가 정합니다. 같은 .App 선택자라도 index.css는 여백을, App.css는 정렬과 색을 정하므로 두 파일의 역할을 함께 읽어야 합니다.
더 깊이: 저장한 뒤에도 state가 남는 조건
Fast Refresh는 고친 파일의 컴포넌트만 새 코드로 바꿔 끼우고, 안전하다고 판단할 때는 그 컴포넌트의 state를 그대로 둡니다. React Native 문서에 정리된 Fast Refresh의 규칙에 따르면 useState와 useRef의 값은 훅에 넘기는 인자나 훅을 부르는 순서를 바꾸지 않는 한 편집 전의 값으로 남습니다. 반대로 의존성 배열을 받는 useEffect, useMemo, useCallback은 Fast Refresh가 일어날 때 의존성을 무시하고 언제나 다시 실행됩니다. 그래서 빈 배열을 넘긴 Effect도 파일을 저장하면 한 번 더 실행될 수 있으며, 이는 고친 코드를 화면에 반영하기 위한 동작입니다.
state가 남는지는 고친 파일이 무엇을 내보내는지(export)에도 달려 있습니다. Vite의 React 플러그인 문서는 Fast Refresh가 제대로 동작하려면 파일이 React 컴포넌트만 내보내야 한다고 안내하고, 내보내는 값이 이 조건에 맞지 않게 바뀌면 그 모듈을 무효로 만든 뒤 갱신을 그 파일을 가져오는 모듈 쪽으로 넘깁니다. 컴포넌트와 함께 내보낸 단순한 상수는 그 값이 바뀔 때만 모듈을 무효로 만들지만, 도우미 함수 같은 다른 값까지 함께 내보내면 그 파일을 고칠 때 컴포넌트의 state가 초기화될 수 있습니다. 함께 쓰는 값을 별도 파일로 옮기면 컴포넌트 파일만 바꿔 끼울 수 있게 됩니다. 또 state를 유지하는 것은 함수 컴포넌트와 훅뿐이며, 클래스 컴포넌트의 state는 Fast Refresh에서 유지되지 않습니다.
정리
index.html의 모듈script가main.jsx를 불러오고,main.jsx가createRoot()로 만든 React 루트에서App의 첫 렌더를 요청합니다.#root는 화면을 그릴 HTML DOM의 대상이고, React 루트는main.jsx가 만든 런타임의 루트입니다.- 컴포넌트가 돌려준 JSX가 화면이 되며, 저장한 변경은 Fast Refresh로 반영되지만 변경의 종류에 따라 전체 새로고침이 일어날 수 있습니다.
- JSX에서는
className,htmlFor같은 정해진 속성 이름을 쓰고, 여러 요소를 돌려줄 때는 하나의 요소나 프래그먼트로 감싸며, 중괄호 안에 자바스크립트 표현식을 넣습니다. - 컴포넌트의 스타일은
import한 CSS 파일로 적용되며, 전역 스타일인index.css의 규칙도 함께 화면에 영향을 줍니다.
연습 문제
1. 중괄호 안의 계산식
앞의 App.jsx에서 첫 번째 <p>만 오늘은 {2 + 3}번째 학습입니다.라는 JSX 내용으로 바꾸고 <h1>은 그대로 둡니다. 저장한 뒤 보이는 첫 번째 <p>와 <h1>은 무엇일까요? 계산식을 감싼 중괄호를 지우면 무엇이 보일까요?
해설 보기
첫 번째 <p>는 “오늘은 5번째 학습입니다.”, <h1>은 “나 혼자 React!”입니다. 중괄호 안의 2 + 3은 자바스크립트 표현식으로 계산됩니다.
중괄호를 지우면 2 + 3은 JSX의 일반 텍스트가 되어 계산되지 않으므로, “오늘은 2 + 3번째 학습입니다.”가 그대로 보입니다.
2. 제목 색만 바꾸면
1번의 상태에서 App.css의 h1 색만 다른 색으로 바꾸면, 첫 번째 <p>의 학습 횟수도 달라질까요? 저장했는데 화면이 바뀌지 않는다면 무엇을 확인해야 할까요?
해설 보기
달라지지 않습니다. 제목의 색을 바꾸는 것은 CSS의 표현만 바꿀 뿐, <p> 안의 계산에는 영향을 주지 않습니다. JSX가 돌려주는 내용과 CSS가 정하는 모양은 서로 다른 단계이지만 같은 화면으로 합쳐집니다.
저장한 뒤에도 바뀌지 않는다면 수정한 파일이 실행 중인 프로젝트의 파일인지, 열어 둔 Local 주소가 그 개발 서버의 주소인지, 브라우저의 첫 오류가 무엇인지를 확인합니다.
이것으로 1장을 마칩니다. 1장에서는 웹 개발이 변해 온 흐름 속에서 React가 나온 배경과 선언적 UI, 컴포넌트, 가상 DOM, 단방향 데이터 흐름이라는 핵심 개념을 살펴보고, Node.js와 Vite로 개발 환경을 갖춘 뒤 첫 React 앱의 화면과 스타일을 직접 바꿔 봤습니다. 다음 장의 JSX 문법부터는 JSX와 컴포넌트, props와 state, 이벤트 처리까지 React의 기초를 하나씩 익힙니다.