[바닐라 챌린지] 회고

jisu·2023년 1월 30일
0

TIL

목록 보기
5/5

넘블 에서 진행하는 챌린지 중 하나에 참여했다. 프로젝트 결과 와 회고를 위주로 작성하려고 한다.

목표

  1. 가장 큰 목표는 바닐라 JS 로 SPA 구현하기였다. Html 여러장이 아닌, 한장의 Html 과 JS 로 프로젝트 구조를 잡으려고 했다.
  2. 번들링을 직접 해보기. Webpack 을 제대로 써본적도 없고, 프로젝트 세팅을 처음부터 온전히(특히 바닐라로) 해본적은 없었기 때문에 config 파일 설정 해봤다.
  3. state, props 변화에 감지하여 컴포넌트 리랜더링. 리액트에서는 자동으로 해주고, useEffect 에만 넣으면 디펜던시를 추적하지만, 바닐라로는 직접 구현해야 했기 때문에 신경 쓸 부분중에 하나였다.

결과

결과물

이미지 사이즈가 커서 랜더링이 오래걸려 보이는 문제가 있었다. 이미지 품질을 낮춘 상태로 랜더링을 하고, 로드가 완료되었을 때 교체 하는 식으로 했으면 좋았을 것 같다. 로딩 스피너를 추가하는 것도 좋았을 것 같다. 시간 관계상 PASS

SPA

router 함수를 만들고, 앱의 시작점에 추가한다. 브라우저 히스토리를 추적할 수 있는 커스텀 이벤트를 만들어서, 히스토리가 변경될 때 마다 그에 맞는 페이지(컴포넌트)를 랜더링 하는 방식으로 간단하게 구현할 수 있었다.

Webpack

프로젝트에서 사용한 웹팩 설정은 다음과 같다. 참고한 자료들은 리드미에 첨부해두었다.

const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const dotenv = require("dotenv").config({
  path: path.join(__dirname, ".env"),
});

module.exports = {
  mode: "development",
  entry: "./src/index.ts", // 👉 스크립트 시작점
  output: { // 👉 빌드된 파일이 생성될 경로
    path: __dirname + "/dist",
    filename: "bundle.js",
    clean: true,
    publicPath: "/",
  },
  devtool: "inline-source-map",
  devServer: {
    static: "./dist",
    historyApiFallback: {
      index: "index.html",
    },
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"], // 👉 import 간단히 할 파일 확장자
    alias: {
      "@": path.resolve(__dirname, "src/"), // 👉 @ 로 import 할 수 있게 한다. ✅ ts 쓰려면 관련 설정을 tsconfig 에도 추가해야 한다.
    },
  },
  plugins: [
    new HtmlWebpackPlugin({ // 👉 번들링 된 html 을 수정하기 위한 속성들
      title: "넘블러 신년메세지 주고받기",
      lang: "ko-KR",
      meta: {
        description: "넘블러 신년메세지 주고받기 챌린지입니다.",
      },
      template: "./src/index.html",
    }),
    new webpack.DefinePlugin({ // 👉 .env 를 브라우저에서 읽기 위한 설정
      "process.env": JSON.stringify(dotenv.parsed),
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/, // 👉.css 확장자로 끝나는 모든 파일을 의미한다
        use: ["style-loader", "css-loader"], // ✅ style-loader를 앞에 추가하자. 아니면 에러가 난다
      },
      {
        test: /\.png\.jpg$/, // 👉 .png 확장자로 마치는 모든 파일
        loader: "file-loader",
        options: {
          publicPath: "./dist/", // 👉 prefix를 아웃풋 경로로 지정
          name: "[name].[ext]?[hash]", // 👉 파일명 형식
        },
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
      {
        test: /\.png\.jpg$/,
        use: {
          loader: "url-loader", // 👉 url 로더를 설정한다
          options: {
            publicPath: "./dist/", // 👉 file-loader와 동일
            name: "[name].[ext]?[hash]", // 👉 file-loader와 동일
            limit: 5000, // 👉 5kb 미만 파일만 data url로 처리
          },
        },
      },
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
};

useState

리액트에서 사용하는 hook인 useState 를 만들어봤다. 전에 functional coding 스터디하면서 배운 내용을 활용했다. 반응형 아키텍쳐 등에서 사용하는 방법으로 특정 액션 후에 실행될 코드들을 중복해서 배치하는 것이 아니라, 어떤 값이 변경되면 그 이벤트에 반응하여 원하는 코드를 실행하는 것이다.

즉, state 나 props 가 변하면 랜더링을 다시 하는 것 같은 작업을 할 수 있다. 함수형 코딩 책에서는 Cell 이라는 이름으로 구현되었는데, 편의상 useState 로 hooks 처럼 만들었다.

import { isEqual, cloneDeep } from "lodash";

const useState = <T>(initialValue: T) => {
  let current = initialValue;
  const watchers: Function[] = []; // 👉 state 가 변경되면 실행될 함수 리스트

  return {
    getValue: () => cloneDeep(current),
    setValue: (newValue: T) => {
      const oldValue = cloneDeep(current);
      if (!isEqual(oldValue, newValue)) {
        current = cloneDeep(newValue);
        watchers.forEach((watcher) => {
          watcher(newValue);
        });
      }
    },
    addWatcher: (watcher: Function) => {
      watchers.push(watcher);
    },
  };
};

export type UseStateType<T> = ReturnType<typeof useState<T>>;

export default useState;

getter, setter 로 값을 다루고 setter 로 값이 변경되면 watcher 를 순회하면서 함수를 순차적으로 수행하는 방식이다. state 가 변경될 때마다 의도대로 render 가 다시 되었지만 결국 중복 코드를 만드는 결과가 생겼다.

리액트 class 형 컴포넌트에서 기본이 되는 Component 처럼 state 와 render 작업을 작성해두고, 실제 커스텀 컴포넌트에서는 이를 상속 받는 방식이 깔끔했을 것 같다. 챌린지에 참여한 다른 사람들의 코드를 보니 그런식으로 구현한 사람이 있었는데, 훤씬 보기 좋았다.

느낀점

결과적으로는 챌린지를 성공하지 못했다. 너무 늦게 시작해서 시간이 부족했고, 그래도 다 완성은 했는데 배포를 못해서 제출을 못했다. 그래도 로컬 실행에서는 정상적으로 작동하는 것을 확인했고, 개인적으로는 배운것들이 많아서 꽤나 성공적인 챌린지라고 생각한다.

프레임워크가 역시 많은 걸 해주고 있다는 것을 새삼 느꼈고, 동작 원리에 대해 다시 생각해보는 계기가 되었다. 스터디에서 배운 내용을 직접 적용해보니까 재미있었고, 책을 더 많이 읽어야겠다는 생각을 살짝 했다.

웹팩 설정은 괜히 겁나고 무슨 소린지 이해가 안갔었는데, 구글링 하면서 하나씩 추가해보니 결국 필요한 필수적인 것들이라는 생각이 들었다. 다음에는 vite 나 만두 같이 생긴걸 써서 해봐야겠다.

profile
(이제부터라도) 기록하려는 프론트엔드 디벨로퍼입니다 XD

0개의 댓글