본문 바로가기

FRONTEND

[React] Learning React Chapter 5 - 웹팩과 바벨 part 2

해당 글은 'Learning React 러닝 리액트 2판(한빛미디어 출판)에 대한 내용을 바탕으로 개인 학습 목적을 위해 작성되었습니다. 잘못된 내용이 있을 수 있으니 참고 부탁드리며 해당 내용에 대해 피드백주시면 반영할 수 있도록 하겠습니다.

 

자 그럼 웹팩을 설정하여 프로젝트를 만들어 보자.

 

프로젝트 시작

먼저 해당 프로젝트를 진행할 폴더를 만든다.

mkdir react-recipes-app
cd react-recipes-app

 

해당 폴더에서 -y 플래그를 사용해 기본적인 패키지를 설치한다.

npm init -y
npm install react react-dom

 

폴더 구조와 컴포넌트

폴더 구조는 다음과 같다.

본격적으로 코드를 작성하기에 앞서, 현재 만드는 애플리케이션에는 데이터가 필요하다. src 폴더에 data 폴더를 만들고 JSON 형식의 데이터를 만들어보자.

// ./src/data/recipes.json

[
  {
    "name": "Baked Salmon",
    "ingredients": [
      { "name": "Salmon", "amount": 1, "measurement": "l lb" },
      { "name": "Pine Nuts", "amount": 1, "measurement": "cup" },
      { "name": "Butter Lettuce", "amount": 2, "measurement": "cups" },
      { "name": "Yellow Squash", "amount": 1, "measurement": "med" },
      { "name": "Olive Oil", "amount": 0.5, "measurement": "cup" },
      { "name": "Garlic", "amount": 3, "measurement": "cloves" }
    ],
    "steps": [
      "Preheat the oven to 350 degrees.",
      "Spread the olive oil around a glass baking dish.",
      "Add the salmon, garlic, and pine nuts to the dish.",
      "Bake for 15 minutes.",
      "Add the yellow squash and put back in the oven for 30 mins.",
      "Remove from oven and let cool for 15 minutes. Add the lettuce and serve."
    ]
  },
  {
    "name": "Fish Tacos",
    "ingredients": [
      { "name": "Whitefish", "amount": 1, "measurement": "l lb" },
      { "name": "Cheese", "amount": 1, "measurement": "cup" },
      { "name": "Iceberg Lettuce", "amount": 2, "measurement": "cups" },
      { "name": "Tomatoes", "amount": 2, "measurement": "large" },
      { "name": "Tortillas", "amount": 3, "measurement": "med" }
    ],
    "steps": [
      "Cook the fish on the grill until hot.",
      "Place the fish on the 3 tortillas.",
      "Top them with lettuce, tomatoes, and cheese"
    ]
  }
]

 

이제 컴포넌트를 만들어 보자.

이전 글에서 말한것과 같이 코드를 역할 단위로 분리하여 작성할 것이다. 보통 컴포넌트의 이름은 대문자로 하는 것이 일반적이다.

 

Recipe.js 파일은 다음과 같이 같이 작성할 수 있다.

// ./components/Recipe.js
import React from "react";

function Recipe({ name, ingredients, steps }) {
  return (
    <section id="baked-salmon">
      <h1>{name}</h1>
      <ul className="ingredients">
        {ingredients.map((ingredient, index) => {
          <li key={index}>{ingredient.name}</li>;
        })}
      </ul>
      <section className="instructions">
        <h2>Cooking Instructions</h2>
        {steps.map((step, index) => (
          <p key={index}>{step}</p>
        ))}
      </section>
    </section>
  );
}

export default Recipe;

 

리액트가 함수형 프로그래밍에 근간을 둔다는 것과 컴포넌트가 담당하는 기능을 좀 더 세분화 시킨다는 관점에서 보았을 때, Recipe 컴포넌트는 좀 더 좁은 형태의 컴포넌트로 세분화 할 수 있다.  Recipe 컴포넌트의 ingredients, instructions 두 부분을 컴포넌트화 시킬 수 있을 것 같다. ingredients 관련 부분도 Ingredient, IngredientList 두 부분으로 컴포넌트화 하였다.

// ./components/Ingredient.js
import React from "react";

function Ingredient({ amount, measurement, name }) {
  return (
    <li>
      {amount} {measurement} {name}
    </li>
  );
}

export default Ingredient;



// ./components/IngredientList.js
import React from "react";
import Ingredient from "./Ingredient";

function IngredientList({ list }) {
  <ul className="ingredients">
    {list.map((ingredient, index) => (
      <Ingredient key={index} {...ingredient} />
    ))}
  </ul>;
}

export default IngredientList;



// ./components/Instructions.js
import React from "react";

function Instructions({ title, steps }) {
  return (
    <section className="instructions">
      <h2>{title}</h2>
      {steps.map((step, index) => (
        <p key={index}>{step}</p>
      ))}
    </section>
  );
}

export default Instructions;



// ./components/Recipe.js 
import React from "react";
import Ingredients from "./Ingredients";
import Instructions from "./Instructions";

function Recipe({ name, ingredients, steps }) {
  return (
    <section id={name.toLowerCase().replace(/ /g, "-")}>
      <h1>{name}</h1>
      <Ingredients list={ingredients} />
      <Instructions title="Cooking Instructions" steps={steps} />
    </section>
  );
}

export default Recipe;

 

위의 코드  IngredientList.js에서 스프레드 연산자로 표현한 것은 다음과 같은 표현이니 참고하자.

// 아래 두 개의 컴포넌트는 동일한 표현이다.
// 스프레드 문법으로 간단하게 코드를 작성할 수 있지만 불필요한 props까지 전부 불러오게된다.

<Ingredient {...ingredient} /> 


<Ingredient 
  amount={ingredient.amount}
  measurement={ingredient.measurement}
  name={ingredient.name}
 />

 

Recipe 컴포넌트를 활용하여 Menu 컴포넌트를 만들어보자.

// ./components/Menu.js

import React from "react";
import Recipe from "./Recipe";

function Menu({ recipes = [] }) {
  return (
    <article>
      <header>
        <h1>Delicious Recipes</h1>
      </header>
      <div className="recipes">
        {recipes.map((props, index) => (
          <Recipe key={index} {...props} />
        ))}
      </div>
    </article>
  );
}

export default Menu;

 

최종적으로 화면을 구성하는 UI 관련 컴포넌트 코드는 다음과 같다.

// ./components/Ingredient.js
import React from "react";

function Ingredient({ amount, measurement, name }) {
  return (
    <li>
      {amount} {measurement} {name}
    </li>
  );
}

export default Ingredient;



// ./components/IngredientList.js
import React from "react";
import Ingredient from "./Ingredient";

function IngredientList({ list }) {
  <ul className="ingredients">
    {list.map((ingredient, index) => (
      <Ingredient key={index} {...ingredient} />
    ))}
  </ul>;
}

export default IngredientList;



// ./components/Instructions.js
import React from "react";

function Instructions({ title, steps }) {
  return (
    <section className="instructions">
      <h2>{title}</h2>
      {steps.map((step, index) => (
        <p key={index}>{step}</p>
      ))}
    </section>
  );
}

export default Instructions;



// ./components/Recipe.js 
import React from "react";
import Ingredients from "./Ingredients";
import Instructions from "./Instructions";

function Recipe({ name, ingredients, steps }) {
  return (
    <section id={name.toLowerCase().replace(/ /g, "-")}>
      <h1>{name}</h1>
      <Ingredients list={ingredients} />
      <Instructions title="Cooking Instructions" steps={steps} />
    </section>
  );
}

export default Recipe;



// ./components/Menu.js
import React from "react";
import Recipe from "./Recipe";

function Menu({ recipes = [] }) {
  return (
    <article>
      <header>
        <h1>Delicious Recipes</h1>
      </header>
      <div className="recipes">
        {recipes.map((props, index) => (
          <Recipe key={index} {...props} />
        ))}
      </div>
    </article>
  );
}

export default Menu;

 

이제 만든 컴포넌트를 렌더링해야한다. 해당 프로젝트를 렌더링하기 위한 주 파일은 index.js 파일이다. index.js 파일을 만들어보자. 

// ./src/index.js

import React from "react";
import { render } from "react-dom";
import Menu from "../components/Menu";
import data from "./data/recipes.json";

render(<Menu recipes={data} />, document.getElementById("root"));

 

아직은 브라우저를 보아도 해당 내용이 렌더링되지 않는다.

 

웹팩으로 빌드하기

이제 여러 파일들을 웹팩 빌드를 통해 모든 파일들을 하나로 만들어보자.

 

아래는 웹팩 사용을 위해 필요한 모듈 설치 명령어이다.

npm install --save-dev webpack-cli

 

또,  바벨 사용을 위해 해당 모듈을 설치한다.

npm install babel-loader @babel/core --save-dev

 

아래의 모듈은 바벨을 실행할 때 사용하는 프리셋 지정에 필요하므로 설치해준다.

npm install @babel/preset-env @babel/preset-react --save-dev

 

기본적으로 프로젝트를 번들링 하기 위해서는 별도의 설정 파일이 필요했지만 웹팩 버전 4.0.0 부터 설정 파일을 만들 필요가 없어졌다. 만약 설정 파일이 없다면 기본값을 사용하여 파일을 번들링한다. 설정 파일을 사용한다면 원하는대로 커스터마이징 할 수 있다는 장점이 있다. 웹팩 설정 파일을 만들어보자. 대부분의 경우, 웹팩 설정 파일의 이름은 webpack.config.js 로 한다.

// ./webpack.config.js

const path = require("path");

module.exports = {
  entry: "./src/index.js", //웹팩에게 클라이언트의 시작 파일이 index.js인 것을 지정
  output: { //번들링을 하게되면 해당 폴더에 해당 이름으로 파일을 생성하도록 지정
    path: path.join(__dirname, "dist", "assets"),
    filename: "bundle.js",
  },
  module: { //모듈을 실행할때 사용할 로더의 목록
    rules: [ // 웹팩에 사용할 여러 유형의 로더를 포함해야 하기 때문에 배열로 작성
      {
       test: /\.js$/, // 로더가 작용해야 하는 파일 경로를 찾기 위한 정규식으로 표시
        exclude: /(node_modules)/,
        use: {
          loader: "babel-loader",
          options: { //바벨 설정 관련 프리셋 지정
            presets: ["@babel/preset-env", "@babel/preset-react"],
          },
        },
      },
      {
        test: /\.css$/i, // 현재 프로젝트에서는 css 로더는 불필요하지만 참고를 위해 작성해두었다. 
        use: ["style-loader", "css-loader"],
      },
    ],
  },
};

 

웹팩이 작동하는 원리는 다음과 같다. 해당 프로젝트의 실행은 index.js 파일을 통해 시작된다. index.js 파일의 위에서 부터 코드를 읽어가면서 React, ReactDOM, Menu.js 파일을 가져온다(import). 브라우저에서 해당 부분을 가장 먼저 실행한다. 웹팩은 'import' 키워드를 발견할 때마다 해당 파일을 찾아 번들링한다. index.js 파일은 Menu.js 파일을 Menu.js 파일은 Recipe.js 파일을 Recipe.js 파일은 Instructions.js, IngredientList.js 파일을 IngredientList.js 파일은 Ingredient.js 파일을  가져온다(import). 이렇게 모든 파일들을 순회하면서 의존 관계를 형성하여 의존 관계 그래프를 생성한다. 이 그래프 전체를 번들(묶음, 모음)이라 한다.

 

이제  package.json 파일에서 script 지정하여 번들링하는 명령어를 변경 해보자.

// ./package.json

//..

"scripts": {
    "start": "serve ./dist",
    "build": "webpack --mode production"
  },
  
//..

 

이렇게 작성하고 npm run build 명령어를 입력하게 되면 dist 폴더가 생성되고 번들링된 파일이 생성된 것을 볼 수 확인할 수 있다. 이렇게 웹팩과 바벨을 사용하여 번들링 작업을 완료했다. 하지만 여기서 끝이 아니다. 번들링된 파일을 화면에 로드해야한다. dist 폴더 안에 index.html 파일이 있어야 리액트의 Menu 컴포넌트를 마운트 시킬 대상인 div 태그를 찾을 수 있다. 아래와 같이 dist 폴더 내에 index.html 파일을 만들어보자.

// ./dist/index/html

<!DOCTYPE html>
<html>
  <head>
    <meta
      name="viewport"
      content="minimum-scale=1.0, width=device-width, maximum-scale=1.0, user-scalable=no"
    />
    <meta charset="utf-8" />
    <title>React Recipes App</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="assets/bundle.js"></script>
  </body>
</html>

 

이제 npm run start 명령어(run은 생략하고 npm start로 실행할 수 있다)를 실행하면 다음과 같은 화면을 볼 수 있다.

 

 

최종적으로 만들어진 폴더구조는 다음과 같다.

 

마치며

이렇게 Chapter 5를 학습하며 배운 내용들을 정리해보았다. CRA(create-react-app) 명령어를 통해 개발 초기환경을 구축하였을때는 알지못했던 것들에 대해 경험하고 이해할 수 있었다. 바벨과 웹팩을 사용해서 프로젝트에 맞게 커스텀화하여 개발환경을 구축할수 있다고 하는데 이 부분은 좀 더 학습과 조사가 필요하다. 이번 챕터를 통해 대략적으로 리액트가 어떤 방식으로 동작하는지 환경 세팅을 어떻게 하고 그동안 당연하게 사용하던 것의 의미는 무엇인지 등을 직접 체험하는 것으로 이해할 수 있었다. 왜 컴포넌트를 좀 더 잘게 분리하고 왜 모듈화 하는지 또 그렇게 하기 위해서는 초기에 프로젝트를 어떻게 설계해야하는지에 대해 다시 한 번 생각할 수 있는 좋은 기회였다.