14

React 프로젝트 직접 설정해보며 이해하기

HTML 파일 생성

우선 다음과 같은 기본 index.html 파일을 생성해 줍니다. React 컴포넌트를 그릴 id가 root인 div 태그도 추가해 주었습니다.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>리액트 프로젝트</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

JavaScript 파일 생성 및 연결

이제 JavaScript 파일을 생성하고

console.log(document.getElementById("root"));

script 태그를 사용해 연결해 줍니다.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>리액트 프로젝트</title>
<script src="index.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

이제 index.html 을 실행해 보면 null이 로그에 찍히고 있음을 확인할 수 있습니다.

console.log(document.getElementById("root")); // null

그 이유는 브라우저가 HTML 파일을 한 줄 한 줄 읽으면서 파싱 하다가 script 태그를 보게 되면 파싱을 중단하고 index.js 파일을 다운로드 및 실행하며 이 과정이 종료되어야만 이어서 HTML을 파싱 하기 때문입니다. 즉, index.js 파일이 실행될 때에는 root라는 id를 가진 div 태그가 DOM에 추가되기 전이기 때문에 null 이 찍힌 것입니다.

이를 해결하기 위해 defer 옵션을 활용할 수 있습니다. defer 옵션을 활용할 경우 HTML을 파싱하다가 script 태그를 보게되면 병렬적으로 script 태그에 명시된 JavaScript 파일을 다운로드받으며 HTML 파싱이 종료된 후 JavaScript 파일을 실행하기 때문에 위 문제를 해결할 수 있습니다.

- <script src="index.js"></script> + <script defer src="index.js"></script>
console.log(document.getElementById("root")); // <div id="root"></div>

React 추가

React를 추가할 수 있는 방법에는 크게 두 가지가 있습니다.

  1. CDN 링크 사용
  2. npm 패키지 설치

CDN 링크 사용

CDN 링크를 사용하여 React를 추가하기 위해서는 해당 코드를 참조하는 script 태그를 추가하면 됩니다. 이전 공식 문서를 참고하여 다음과 script 태그를 추가해 줍니다.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>리액트 프로젝트</title>
<script defer crossorigin src="https://unpkg.com/react@18/umd/react.development.js" ></script>
<script defer crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" ></script>
<script defer src="index.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

defer 옵션을 사용하였기 때문에 script 태그에 명시된 JavaScript 코드를 병렬적으로 다운로드하고 HTML 파싱이 종료된 후 문서상의 순서에 따라 순차적으로 실행됩니다. 따라서 index.js가 실행되는 시점에는 React, ReactDOM이 이미 다운로드 된 상태임이 보장됩니다.

그러면 index.js에는 window 객체에 React 및 ReactDOM 객체가 추가되며 이를 활용하여 코드를 작성할 수 있습니다.

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render("안녕");

그러면 브라우저로 index.html을 실행했을 때 정상적으로 루트에서 “안녕”이라는 텍스트를 보여주는 것을 확인할 수 있습니다.

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

CDN 링크를 사용할 때의 문제점

CDN 링크를 사용해서 프로젝트를 계속해서 확장한다고 생각할 경우 아쉬운 점이 많습니다. 예를 들면 다음과 같은 문제가 있습니다.

  1. 사용하는 라이브러리가 HTML 파일의 script 태그에 명시되어 있기 때문에 어떤 JavaScript 파일에서 어떤 라이브러리에 의존하는지 파악하기 어렵습니다.
  2. 라이브러리 코드의 일부만 사용할 경우에도 전체 라이브러리 코드를 다운로드 받기 때문에 사용자가 프로젝트를 실행하기 위해 실제 필요한 것보다 많은 JavaScript 코드를 다운로드 받야야합니다.
  3. script 파일에 명시된 코드를 잘못된 순서로 실행할 경우 코드가 의도한대로 동작하지 않을 수 있습니다.
  4. 많은 라이브러리에 의존할 경우 병렬적으로 다운로드해도 한 번에 개수 제한이 있기 때문에 로딩시간이 길어집니다.
  5. window 전역 객체에 라이브러리 변수가 추가되기 때문에 변수의 충돌이 일어날 가능성이 있습니다.

npm 패키지 설치

이러한 문제들도 해결하고 JavaScript 모듈을 효율적으로 사용하기 위해 npm으로 패키지를 설치하고 Webpack과 같은 번들러를 사용하는 것이 효과적입니다. 이를 통해 코드 의존성을 명확히 파악하고 필요한 코드만 다운로드하여 프로젝트를 효율적으로 관리할 수 있습니다.

이제 필요한 npm 패키지를 설치해 보겠습니다.

// 1. npm init -y // 기본 package.json 파일을 생성합니다.
// 2. npm add react react-dom // react, react-dom 패키지를 설치합니다.
// 3. npm add -D webpack webpack-cli // webpack 그리고 webpack 명령어를 사용하기 위한 webpack-cli를 설치합니다.
// 개발 환경에서만 필요하기 때문에 -D 옵션을 사용해 devDependencies에 포함시켜주면 프로덕션 빌드 시 번들에 포함되지 않습니다.

여기까지 입력을 완료하면 다음과 같은 package.json 파일이 생성됩니다.

{
"name": "setting-up-react-project-from-scratch",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4"
}
}

이제 webpack.config.js를 생성하여 Webpack의 옵션을 설정해 주겠습니다.

const path = require("path");
module.exports = {
// Webpack을 통해 번들링하고자하는 파일인 index.js의 상대 경로를 입력해 줍니다.
entry: "./index.js",
output: {
// 번들링이 완료된 JavaScript 파일을 어떤 폴더에 생성할지, 어떤 이름으로 생성할지 작성해 줍니다.
// dist라는 이름의 폴더에 생성하고 bundle.js라는 이름을 갖도록 설정해 주겠습니다.
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
};

다시 기존의 index.html 파일로 돌아가 기존의 script 태그를 수정해 줍니다.

- <script - defer - crossorigin - src="https://unpkg.com/react@18/umd/react.development.js" - ></script> - <script - defer - crossorigin - src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" - ></script> - <script defer src="index.js"></script> + <script defer src="dist/bundle.js"></script>

index.js 파일로 돌아가 import로 필요한 모듈만 가져와서 다음과 같이 코드를 변경해 줍니다. CDN 링크를 사용했을 때와는 달리 의존성이 명확하게 드러나게 되었습니다.

import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render("안녕");

다음 명령어를 입력하면 webpack.config.js에 작성한 옵션에 따라 번들링이 완료되고

npx webpack

브라우저에서 다음과 같은 결과를 확인할 수 있습니다.

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

JSX 사용

JSX를 사용하여 JavaScript 파일에서 HTML 문법으로 마크업을 해보겠습니다.

import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(<h1>안녕</h1>);

기존의 index.htmlh1 태그로 감싼 후 npx webpack을 통해 번들링을 시도하면 기대와는 달리 번들에 실패합니다.

Webpack은 JavaScript와 JSON 파일만 이해할 수 있습니다. Webpack의 입장에서는 <h1>안녕</h1>이 무엇인지 이해할 수 없습니다. 따라서 JSX 문법으로 작성된 코드를 JavaScript 코드로 변환해줄 필요가 있습니다. Webpack의 로더 그리고 babel과 babel에서 제공하는 @babel/preset-react를 사용하여 이를 해결해 보겠습니다.

우선 Webpack에서 babel을 사용할 수 있도록 babel-loader를 설치하고 JSX 문법으로 작성된 코드를 JavaScript 코드로 변환해주는 플러그인이 포함되어있는 @babel/preset-react을 설치해 줍니다.

npm add -D babel-loader @babel/preset-react

webpack.config.js 으로 돌아가 .js로 끝나는 모든 파일은 babel을 통해 변경 사항을 적용하도록 지시해 줍니다.

const path = require("path");
module.exports = {
entry: "./index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [{ test: /\.js$/, use: "babel-loader" }],
},
};

그리고 다음과 같은 babel.config.json 을 생성하면 babel이 어떤 변경 사항을 적용할지 지시할 수 있습니다. 해당 프리셋에는 JSX로 작성된 코드를 JavaScript로 작성된 코드로 변경하는 플러그인이 포함되어 있기 때문에 원하는 목적을 달성할 수 있습니다.

{
"presets": ["@babel/preset-react"]
}

이제 npx webpack 명령어를 통해 번들링을 시도하면 동작할 것이라고 생각했지만 HTML를 열어서 확인해 보면 빈 화면을 보게 됩니다. 그리고 콘솔을 보면 다음과 같은 에러를 확인할 수 있습니다.

Uncaught ReferenceError: React is not defined

그 이유는 다음과 같이 JSX 문법으로 작성한 코드는 babel이 React.createElement를 사용하는 JavaScript 코드로 변환해 주며 index.js 파일에서 React를 import 하지 않았기 때문입니다.

// JSX 문법으로 작성한 코드
<h1>안녕</h1>
// babel이 변환해준 코드
React.createElement("h1", null, "안녕");

따라서 index.js 파일에 React 를 import하는 코드를 추가한 후 다시 npx webpack 명령어로 번들링 하면 다음과 같이 정상적으로 JSX 코드로 작성한 마크업과 일치하는 마크업이 루트에 추가되는 것을 확인할 수 있습니다.

<div id="root">
<h1>안녕</h1>
</div>

React import 없이 사용하는 JSX

React v17부터는 JSX 문법으로 작성한 코드를 JavaScript 코드로 변환할 수 있는 더 좋은 방법을 지원하기 시작합니다.

새로운 방법을 사용하면

  1. 더 이상 React를 import 하지 않아도 JSX 문법을 사용할 수 있습니다.
  2. 변환된 코드의 번들 사이즈가 조금 더 줄어들게 됩니다.

다음과 같이 JSX 문법으로 코드를 작성하면

function App() {
return <h1>안녕</h1>;
}

React.createElement 가 아닌 _jsx라는 새로운 API를 사용하며 이는 babel에 의해 자동으로 주입됩니다.

import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: '안녕' });
}

기존의 코드가 이 방법을 사용하도록 변경해 보겠습니다.

우선 더 이상 React를 import할 필요가 없기 때문에 index.js에서 제거해 줍니다.

import { createRoot } from "react-dom/client";
- import React from 'react';
const root = createRoot(document.getElementById("root"));
root.render(<h1>안녕</h1>);

그리고 babel.config.json에서 runtime이라는 옵션을 automatic으로 설정을 해줍니다.

{
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
]
}

npx webpack 으로 번들링 한 후 확인해 보면 동일한 결과물을 확일할 수 있습니다.

<div id="root">
<h1>안녕</h1>
</div>