[TIL] 211206

📝 오늘 한 것

  1. user profile 복습

  2. webpack - babel-loader / SCSS loader / MiniCssExtractPlugin


📚 배운 것

1. user profile 복습

처음부터 다시 구현해본 후 헷갈렸거나 다시 보고 싶은 부분 정리

1) DB 업데이트 / session 업데이트

model.findByIdAndUpdate()는 업데이트 전의 데이터를 return 한다.
업데이트 후의 데이터를 받기 위해서는 { new: true } 옵션을 줘야 한다.

2) multer 사용

파일을 업로드 하기 위해서는 express가 파일 경로를 알 수 있도록 추가하고, express.static("브라우저에 노출시킬 폴더 이름")을 사용해야 한다.

app.use("/uploads", express.static("uploads"));

3) video owner profile

video를 업로드한 user의 profile은 누구나 볼 수 있도록 req.session.user._id가 아니라 req.params.id를 사용해야 한다.

4) loggedInUser is undefined

// watch.pug

// 생략
  if String(video.owner._id) === loggedInUser._id
    a(href=`${video.id}/edit`) Edit Video →
    a(href=`${video.id}/delete`) Delete Video →

위 코드의 결과, 로그인 하지 않은 user가 home에서 video.title을 클릭해 watch 페이지에 들어가려고 하면, loggedInUser가 undefined라서 에러가 뜨는 문제가 발생한다.

// middlewares.js
export const localsMiddleware = (req, res, next) => {
  res.locals.siteName = "Wetube";
  res.locals.loggedIn = true;
  res.locals.loggedInUser = req.session.user || {}; // 이 부분에 || {} 를 추가했다 ❗
};

localsMiddleware에서 user가 로그인하지 않아도 loggedInUser가 true 값을 가질 수 있도록 수정해야 한다.

5) req.session.loggedIn과 res.locals.loggedInUser 비교

// middlewares.js
export const localsMiddleware = (req, res, next) => {
  res.locals.siteName = "Wetube";
  res.locals.loggedIn = Boolean(req.session.loggedIn);
  res.locals.loggedInUser = req.session.user || {};
  next();
};

export const protectorMiddleware = (req, res, next) => {
  if (res.locals.loggedInUser) { // req.session.loggedIn으로 변경 ❗
    next();
  } else {
    return res.redirect("/login");
  }
};

export const publicOnlyMiddleware = (req, res, next) => {
  if (!res.locals.loggedInUser) { // req.session.loggedIn으로 변경 ❗
    next();
  } else {
    return res.redirect("/");
  }
};

웹 사이트의 어떤 페이지에 들어갈 때도 localsMiddleware가 실행되기 때문에 req.session.user의 값에 따라 loggedInUser의 값이 달라질 수도 있지만

앞서 localsMiddleware에서 loggedInUser의 값을 무조건 true가 되도록 || {}을 추가했기 때문에
위와 같이 protectorMiddleware와 publicOnlyMiddleware에서 res.locals.loggedInUser를 사용하면 그 값이 무조건 true가 되어 에러가 발생한다.

따라서, res.locals.loggedInUser 대신에 req.session.loggedIn을 사용해야 한다.


2. Webpack

1) Webpack이란?

Webpack이란 자바스크립트나 CSS, 이미지 등의 리소스들을 변환하고 묶어주는 모듈 번들러를 말한다.

다만, react.js, vue.js 등 대부분의 큰 프레임워크에는 webpack이 이미 내장되어 있기 때문에 실제로 webpack을 직접 작성해볼 일은 거의 없다.

그러나, 이는 업계 표준으로써 최소한 어떻게 작동하는지는 알아야 하기 때문에 이해를 위해 webpack configuration 파일을 작성해보려고 한다.

2) webpack 설치 및 사용법

(1) 설치

npm을 이용해 webpack과 webpack cli를 devDependencies에 설치한다.
webpack cli를 이용해 콘솔에서 webpack을 불러낼 수 있다.

$ npm i webpack webpack-cli --save-dev

(2) entry / output

프로젝트 폴더에 webpack.config.js 파일을 만든다.
이 파일에는 최신 코드를 사용해서는 안된다.

export default, import (x)
module.exports = {}, const ~ require() (o)

webpack을 사용하기 위해서는 entry와 output을 반드시 지정해줘야 한다.
entry란 webpack을 거쳐 변형시키고자 하는 파일의 경로를 말한다.
output이란 webpack을 거쳐 변형된 결과물을 저장할 파일명과 그 파일이 저장될 경로를 말한다.

이때 그 파일이 저장될 경로는 절대 경로로 작성해야 한다.
이를 위해 path.resolve(__dirname, "")를 이용한다.
path.resolve()는 입력한 모든 파트들을 모아 경로로 만들어준다.
dirname이란 root부터 폴더까지의 경로 전체를 의미한다.

실습을 위해 아래와 같이 설정한 후, src/client/js 폴더 안에 main.js 파일을 만들어 최신 코드를 작성했다.

// webpack.config.js
const path = require("path");

module.exports = {
  entry: "./src/client/js/main.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "assets", "js"),
  },
};

package.json 파일에서 assets script를 만든 후 콘솔에서 실행한다.

// package.json
"scripts": {
  "assets": "webpack --config webpack.config.js"
},
$ npm run assets

자동으로 assets/js 폴더와 함께 그 안에 변형된 코드가 담긴 main.js 파일이 만들어진 것을 확인할 수 있다.
( + assets 폴더를 .gitignore 파일에 추가하여 github에 업로드하지 않도록 한다.)

이 코드는 제대로 작동하고 있지만, 일부 코드는 브라우저가 이해하지 못할 수도 있기 때문에 호환성을 확보해야 한다.

따라서, 앞서 백엔드 코드 처리를 위해 package.json 파일에서 babel을 사용했듯이, 프론트엔드 코드 처리를 위해 webpack.config.js 파일에서 babel을 사용해야 한다.

(3) rules (모든 js 파일에 babel-loader 적용)

특정 종류의 파일에 특정 변형을 적용하기 위해 rules를 사용해야 한다.
webpack은 loader를 통해 파일을 전환시킨다.
이 경우에는 JavaScript 코드(test)를 babel-loader(use, loader)를 이용해 변환해야 한다.

babel-loader을 이용하기 위해서는 다음 모듈들을 설치해야 한다.
나머지는 모두 설치되어 있으므로 babel-loader만 따로 설치해주었다.

npm install -D babel-loader @babel/core @babel/preset-env webpack

babel-loader 사용법은 아래와 같다.
babel-loader를 참고해 webpack.config.js 파일을 수정했다.

// webpack.config.js
const path = require("path");

module.exports = {
  // 생략
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [["@babel/preset-env", { targets: "defaults" }]],
          },
        },
      },
    ],
  },
};

이제 webpack은 babel-loader에 몇 가지 옵션을 전해주면서 이를 이용해 모든 js 파일을 변형하도록 한다.

다시 assets/js에서 main.js 파일을 확인해보면, babel-loader에 의해 JavaScript 코드가 한번 더 변환된 것을 확인할 수 있다.

(4) mode 옵션

npm run assets을 실행하여 assets 폴더를 만들었을 때부터 콘솔 창에 mode option이 설정되지 않았다는 경고 문구가 떠 있다.

이를 해결하기 위해 webpack에게 이 코드가 지금 개발 중인지 아닌지를 알려줘야 한다.
mode를 설정하지 않으면 webpack은 기본적으로 production mode로 설정되어 모든 코드들을 압축함으로써 에러를 찾기 힘들기 때문이다.

따라서, 현재는 development mode로 설정한 후, 나중에 서버에 백엔드를 직접 올릴 때 이를 바꿔볼 것이다.

const path = require("path");

module.exports = {
  entry: "./src/client/js/main.js",
  mode: "development", // 추가 ❗
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "assets", "js"),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [["@babel/preset-env", { targets: "defaults" }]],
          },
        },
      },
    ],
  },
};

(5) 브라우저에 assets 폴더 공개

아직 express는 assets라는 폴더의 존재를 모르고, 브라우저도 해당 폴더에 접근할 수 없다.
server.js 파일에서 express.static()을 이용해 브라우저가 assets 폴더에 접근할 수 있도록 한다.

이때 경로의 이름은 어떤 이름으로든 작성할 수 있지만, 폴더 이름은 위에서 만든 폴더 이름 그대로 작성해야 한다.
예를 들어, 아래와 같이 작성했다면 assets 폴더의 내용을 /static 주소를 통해 공개하도록 하는 것이다.

// server.js
app.use("/static", express.static("assets"));

base.pug 파일이 main.js 파일을 불러올 수 있도록 assets/js/main.js를 base.pug와 연결한다.
브라우저는 /static ~ 경로에서 assets 폴더 내용을 볼 수 있다.

//- base.pug

// 생략
include partials/footer
script(src="/static/js/main.js")

이제 웹 사이트의 어떤 페이지에 들어가든 main.js 파일의 내용이 실행된다.
/static/js/main.js는 node.js 코드가 아니라 '브라우저에서 작동하는 js 코드'이다.

💡 정리하면

📌 src/client/js/main.js에 최신 js 코드를 작성하면
📌 webpack.config.js에 따라 그것의 변형된 결과물이 assets/js/main.js에 담기게 된다.
📌 pug 파일은 assets 폴더로부터 그 결과물(js 코드)을 불러온다.

3) SCSS loader

(1) scss 파일 만들기

client 폴더 안에 scss 폴더를 만든 후 그 안에 styles.scss 파일과 _variables.scss 파일을 만든다.

// _variables.scss
$red: red;
// style.scss
@import "./_variables";

body {
  background-color: $red;
}

client 폴더 안의 js 폴더 안의 main.js 파일에서, client 폴더 안의 scss 폴더 안의 styles.scss 파일을 import 한다.

// main.js
import "../scss/styles.scss";

console.log("hi");

(2) rules

loader를 지정할 때는 위에서 작성한 것처럼 할 수도 있지만, 여러 개의 loader를 모아서 지정할 수도 있다.

예를 들어, scss 파일을 처리하기 위해서는 다음과 같은 세 개의 loader가 필요하다.

  • sass-loader: scss 파일을 일반 css 파일로 변환
$ npm install sass-loader sass webpack --save-dev
  • css-loader: scss 파일의 @import와 url()을 import/require()로 풀어서 해석
$ npm install --save-dev css-loader
  • style-loader: 변환된 css를 브라우저에 적용 (css를 DOM에 주입)
$ npm install --save-dev style-loader

위 3개의 loader를 아래와 같이 한 번에 모아서 적어줄 수 있다.
이때 주의할 점은 사용 순서와 반대로 적어야 한다는 것이다.
즉, 가장 처음 사용될 loader를 가장 나중에 적어야 한다.

// webpack.config.js
const path = require("path");

module.exports = {
  entry: "./src/client/js/main.js",
  mode: "development",
  output: path.resolve(__dirname, "assets", "js"),
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [["@babel/preset-env", { targets: "defaults" }]],
          },
        },
      },
      {
        test: /\.scss$/,
        use: ["style-loader", "css-loader", "sass-loader"],
      },
    ],
  },
};

이제 콘솔에 npm run assets를 입력하면

webpack은 entry 파일(main.js)을 가져온다.
webpack은 그 main.js 파일이 JavaScript 파일임을 인식한 후, 이를 babel을 이용해 변환하는데
main.js 파일에서 import 해준 파일이 scss 파일임을 인식한 후, scss 파일을 scss, css, style loader를 이용해 css 파일로 변환하고 이를 웹 사이트의 head 안에 입력해 브라우저에 적용시킨다.

(3) MiniCssExtractPlugin

CSS를 추출해서 다른 파일로 분리하기 위해 MiniCssExtractPlugin을 설치한다.

$ npm install --save-dev mini-css-extract-plugin

이는 style-loader를 대신하여 다음과 같이 사용할 수 있다.
코드 양식이기 때문에 외울 필요는 없다. ( MiniCssExtractPlugin 참고 )

// webpack.config.js
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 추가 ❗

module.exports = {
  entry: "./src/client/js/main.js",
  mode: "development",
  plugins: [new MiniCssExtractPlugin()], // 추가 ❗
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "assets", "js"),
  },
  module: {
    rules: [
      // 생략 (babel-loader)
      {
        test: /\.scss$/, // 모든 scss 파일
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], // 수정 ❗
      },
    ],
  },
};

다시 npm run assets를 실행하면 assets 폴더 안의 js 폴더 안에 main.js 파일과 더불어 main.css 파일이 생긴다.

그런데 js와 css가 분리되기는 했지만, assets 폴더 안의 js 폴더에 함께 들어가 있다.

css 파일은 css 폴더에, js 파일은 js 폴더에 넣기 위해
webpack.config.js 파일의 output 부분에서 filename을 수정하고
MiniCssExtractPlugin 부분에 filename 옵션을 넣어준다.

module.exports = {
  entry: "./src/client/js/main.js",
  mode: "development",
  plugins: [
    new MiniCssExtractPlugin({
      filename: "css/styles.css", // 추가 ❗
    }),
  ],
  output: { 
    filename: "js/main.js", // 수정 ❗
    path: path.resolve(__dirname, "assets"), // "js" 삭제 ❗
  },
  // 생략
};

(4) pug 파일과 css 파일 연결

base.pug 파일이 style.css 파일을 불러올 수 있도록 assets/css/style.css를 base.pug와 연결한다.

//- base.pug

doctype html
html(lange="ko")
  head
    title #{pageTitle} | #{siteName}
    link(rel="stylesheet" href="https://unpkg.com/mvp.css")
    link(rel="stylesheet" href="/static/css/styles.css")

// 생략
  include partials/footer
  script(src="/static/js/main.js")

💡 다시 한번 더 정리하면

📌 webpack에 의해서 client 폴더 내의 파일들이 로딩된다.
📌 webpack을 거친 후의 결과물들은 assets 폴더 안에 만들어진다.
📌 그 결과물들은 base.pug 파일에서 로딩되어 브라우저에 보이게 된다.

4) 개발 환경 개선

(1) 저장할 때마다 자동 업데이트

지금으로썬 scss나 프론트엔드 쪽 js 파일에서 코드를 변경할 때마다 콘솔에 npm run dev를 입력해야만 업데이트가 된다.
또한 명령어를 실행하기 전에 혹시 모를 에러를 방지하기 위해 항상 assets 폴더를 지워줘야만 한다.

저장할 때마다 자동으로 업데이트가 되도록 watch function을 사용하도록 한다.
output 폴더가 만들어지기 전에 기존의 output 폴더가 삭제되도록 clean 옵션을 사용하도록 한다.

// webpack.config.js
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  entry: "./src/client/js/main.js",
  mode: "development",
  watch: true,
  plugins: [
    new MiniCssExtractPlugin({
      filename: "css/style.css",
    }),
  ],
  output: {
    filename: "js/main.js",
    path: path.resolve(__dirname, "assets"),
    clean: true,
  },
  module: {
    rules: [
      {
        test: /\.js$/, // 모든 js 파일
        use: {
          loader: "babel-loader",
          options: {
            presets: [["@babel/preset-env", { targets: "defaults" }]],
          },
        },
      },
      {
        test: /\.scss$/, // 모든 scss 파일
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
      },
    ],
  },
};

이제 항상 두 개의 콘솔을 함께 구동해야 한다.

$ npm run dev
$ npm run assets

(2) webpack에 의해 서버가 재시작하지 않도록

그런데 이렇게 하면 프론트엔드 자바스크립트 코드를 변경한 후 저장을 할 때마다 nodemon에 의해 백엔드가 재시작된다.

문제를 해결하기 위해 일부 파일 및 폴더는 변경되더라도 nodemon이 이를 무시하고 서버를 재시작하지 않도록 해야 한다.

이를 위해 프로젝트 폴더에 nodemon.json 파일을 생성한 후 아래와 같이 작성한다. (https://github.com/remy/nodemon 참고)

// nodemon.json
{
  "ignore": ["webpack.package.js", "src/client/*", "assets/*"],
  "exec": "babel-node src/init.js"
}

그 후, package.json 파일에서 dev script (nodemon -L --exec ...)를 수정한다.
( + assets script도 수정한다. webpack.config.json 파일은 webpack이 실행될 때 기본적으로 찾는 설정 파일이기 때문에 굳이 뒤에 적어주지 않아도 된다. )

// 수정 전 package.json
"scripts": {
  "dev": "nodemon -L --exec babel-node src/init.js",
  "assets": "webpack --config webpack.config.json"
}
// 수정 후 package.json
"scripts": {
  "dev:server": "nodemon -L",
  "dev:assets": "webpack"
}

script 이름도 직관적으로 바꿔주었다.
항상 각각 다른 콘솔에서 두 개의 명령어를 모두 실행해야 한다.
하나는 서버를 실행시키고 템플릿과 url을 확인하기 위해 필요하고, 다른 하나는 client 파일을 확인하기 위해 필요하다.


✨ 내일 할 것

  1. 강의 계속 듣기

좋은 웹페이지 즐겨찾기