이미지 사이즈가 커서 랜더링이 오래걸려 보이는 문제가 있었다. 이미지 품질을 낮춘 상태로 랜더링을 하고, 로드가 완료되었을 때 교체 하는 식으로 했으면 좋았을 것 같다. 로딩 스피너를 추가하는 것도 좋았을 것 같다. 시간 관계상 PASS
router 함수를 만들고, 앱의 시작점에 추가한다. 브라우저 히스토리를 추적할 수 있는 커스텀 이벤트를 만들어서, 히스토리가 변경될 때 마다 그에 맞는 페이지(컴포넌트)를 랜더링 하는 방식으로 간단하게 구현할 수 있었다.
프로젝트에서 사용한 웹팩 설정은 다음과 같다. 참고한 자료들은 리드미에 첨부해두었다.
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/,
},
],
},
};
리액트에서 사용하는 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 나 만두 같이 생긴걸 써서 해봐야겠다.