우아한 테크러닝 3기

✔ 우아한 테크러닝 3기: React & TypeScript 6회차

seungmin2 2020. 9. 20. 20:45

✌ 9월17일 (목) 우아한 테크러닝 3기 6회차 강의 정리

🚀 6회차 강의 목표

  • webpack에 대한 흐름 이해하기
  • React & Typescript 살펴보기
  • redux-saga 사용하기

🚀 Webpack 흐름 이해하기

  • Webpack은 Webpack Cli라고 하는 도구가 존재한다.
  • 단지 Hello world를 찍기 위해서 필요한 설정들이 이렇게나 많이 필요하다
  • @babel/core과 같은 @가 붙여져 있는 것들은 트랜스파일링 할때 사용된다. 일종의 플로그인이다.

  • webpackwebpack.config.js라는 이름을 갖는 설정 파일을 갖는다. 이름 변경될 수 있지만 관례적으로 사용된다.
  • webpackwebpack.config.js읽어서 실행시켜주는데 실행시켜줄 때 config객체를 export하게 된다. 그 때 webpack은 받아서 config에 있는 내용대로 실행된다.
const config = {
...
}
module.exports = config;
  • webpack은 기본적으로 node에서 실행되기 때문에 require를 사용한다.
const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const { TsconfigPathsPlugin } = require("tsconfig-paths-webpack-plugin");

🌈 Loader

  • Loader는 리덕스의 미들웨어와 같은 역할을 한다고 생각하면 된다.
  • 첫번째 파일 entry 부터 읽어들이는 파일들을 loader한테 던저주고 loader가 역할을 수행하고 다시 리턴을 해주는 것을 반복한다. (미들웨어의 동작과 비슷)
const config = {
  ...
  // entry
  entry: {
    main: ["core-js", `./src/index.tsx`],
  },
  ...
};
  • loader 설정은 module 속성 안에 작성되고 rules라는 배열안에 객체로 작성된다.
const config = {
  // ...
  module: {
    rules: [
      {
        // 어떤 확장자를 가진 파일만 받을 껀지 정규표현식으로 작성할 수 있다.
        test: /\.(ts|js)x?$/,
        // 어떤 것을 포함시킬지
        include: path.resolve("src"),
        // 어떤 것을 제외 시킬지
        exclude: /node_modules/,
        use: {
          // 위에서 찾은 설정 파일들을 babel-loader 한테 전달해준다.
          loader: "babel-loader",
        },
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        loader: "file-loader",
        options: {
          name: "[name].[ext]",
          outputPath: "./images/",
        },
      },
    ],
  },
  // ...
};

🌈 Plugin

  • Plugin은 loader보다 훨씬 복잡하다.
    복잡한 이유는 webpack의 안쪽에 있는 Low-Level의 기능들을 plugin한테 다 노출을 시켜주고 plugin이 할 수 있게 한다.
    그렇기 때문에 훨씬 더 많은 일을 할 수 있다.
const config = {
  // ...
  plugins: [
    new webpack.SourceMapDevToolPlugin({}),
    new CleanWebpackPlugin(),
    // Html 플러그인으로 작성
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html"),
    }),
    // 환경변수값을 주입하는 플러그인
    new webpack.DefinePlugin({
      "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
    }),
  ],
  // ...
}
  • 보통의 plugin은 loader의 프로세스가 다 끝나고 실행된 다음에 그 결과물을 가지고 plugin이 작업을 한다.
  • 그렇기 때문에 loader는 컨버팅이고 plugin은 후처리라고 생각하면 된다.

🌈 각각의 webpack 설정 파일들을 쪼개는 이유는??

babel도 설정파일을 따로 생성해 사용한다. (.babelrc)
애플리케이션을 만들때 관심사 분리라는 맥락으로 컴포넌트를 잘게 쪼개는데 webpack 설정 파일들도 똑같다.
plugins하나에 하나의 일, loader도 하나에 하나의 일을 하게 디자인하는게 유지보수에 좋다라고 되어있다.
그래서 종류가 굉장히 많다.

🌈 Babel?

  • .babelrc
{
    "sourceType": "unambiguous",
    "presets": [
        [
            "@babel/preset-env",
            {
                "modules": false,
                "useBuiltIns": "usage",
                "corejs": {
                    "version": 3,
                    "proposals": true
                }
            }
        ],
        // jsx의 태그 방식을 파싱해서 함수 호출 방식으로 트랜스파일링 해주는 것을 담당
        "@babel/preset-react",
        "@babel/preset-typescript"
    ],
    "plugins": [
        "@loadable/babel-plugin",
        "babel-plugin-styled-components",
        [
            "@babel/transform-runtime",
            {
                "corejs": 3
            }
        ],
        "@babel/plugin-proposal-optional-chaining",
        ["@babel/plugin-proposal-decorators", { "legacy": true }],
        "@babel/plugin-proposal-class-properties",
        "@babel/plugin-transform-exponentiation-operator",
        "@babel/proposal-object-rest-spread",
        "@babel/plugin-transform-async-to-generator"
    ]
}
  • 결국 Babel안에 plugin이다. Babel 자체도 각종 언어 종류를 트랜스파일링하는 구조들을 또 plugin이라는 형식으로 가지고 있다. 그러니 Babel이 다해주는 것이 아니고 Babel안에 또 plugin들이 존재하는 구조이다.

📌 인프런 강의 듣고 Webpack에 대해서 정리한 것입니다. 참고
📌 캡틴 판교님의 webpack 핸드북
📌 인프런 강의하신 김정환님의 블로그

🚀 React & Typescript 살펴보기

김민태님 CodeSandBox 참고 예제

  • redux는 특정 프레임워크나 라이브러리에 종속되어있는 상태관리자가 아니다. 그래서 vanilla.js에서 사용도 가능하고 원한다면 Vue.js, Angular.js에서도 사용이 가능하다.
import { createStore, applyMiddleware } from "redux";
  • 그렇기 때문에 react에서 직접 사용할려면 react-redux를 사용하면 된다.
  • 리액트의 Context API를 이용해서 내부적으로 구현된Provider`를 사용한다.
  • <App />Provider로 감싸주고 store를 주입시켜주면 App 하위 어디서든 꺼내서 사용할 수 있다.
import { Provider } from "react-redux";
//...
render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);
  • reducer 상태 값을 나타낼 인터페이스이다.
// types
export interface StoreState {  // typealias으로 변경해도 상관없다.
    monitoring: boolean;
    success: number;
    failure: number;
}
// reducers
const initializeState: StoreState = {
  monitoring: false,
  success: 0,
  failure: 0
};
export default (
  state: StoreState = initializeState,
) => {
// ...
}

🌈 typesafe-actions

  • typesafe-actions 라이브러리의 createAction을 사용해서 actions을 생성할 수 있다.
import { createAction } from "typesafe-actions";

export const startMonitoring = createAction(
  // action 이름(속성) 상수 선언이 필요없다.
  // 타입 속성에 넣는다.
  "@command/monitoring/start",
  resolve => {
    return () => resolve();
  }
);

export const stopMonitoring = createAction(
  "@command/monitoring/stop",
  resolve => {
    return () => resolve();
  }
);

export const fetchSuccess = createAction("@fetch/success", resolve => {
  return () => resolve();
});

export const fetchFailure = createAction("@fetch/failure", resolve => {
  return () => resolve();
});
  • 그래서 typesafe-actions를 사용하면 위와 같이 createAction 첫 번째 인자로 상수를 사용하지 않고 문자열로 넘길 수가 있다. 대신 상수역할을 해준다.
  • 위에서 createAction의 두 번째 인자는 데이터가 존재하면 payload라는 속성으로 객체로 데이터를 넣어주고 어떤 타입인지를 감지하고 타이핑까지 해줘서 아래의 reducer 에서 액션에서 어떤 속성인지 확인할 수 있다.
  • typesafe-actions 사용하면 상수선언이 생략되고 type과 payload 만드는 한꺼번에 처리를 해줘서 편리하게 처리가 가능하다.
  • typesafe-actions과 비슷한 역할을 하는 redux-toolkit도 존재하며 리덕스에서 사용하라고 공식문서에도 나온다.
export default (
  state: StoreState = initializeState,
  action: ActionType<typeof Actions>
) => {
  switch (action.type) {
    case getType(Actions.fetchSuccess):
      return {
        ...state,
        success: state.success + Math.floor(Math.random() * (100 - 1) + 1)
      };
    case getType(Actions.fetchFailure):
      return {
        ...state,
        failure: state.failure + Math.floor(Math.random() * (2 - 0))
      };
    default:
      console.log(action.type);
      return Object.assign({}, state);
  }
};

🌈 redux-saga

  • 현재 미들웨어로 redux-saga를 사용하고 있다.
// index.tsx
import * as React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import { StoreState } from "./types";
import reducer from "./reducers";
import createSagaMiddleware from "redux-saga";
import rootSaga from "./sagas";
import App from "./App";

const sagaMiddleware = createSagaMiddleware();
// sagaMiddleware
const store: StoreState = createStore(reducer, applyMiddleware(sagaMiddleware));
const rootElement: HTMLElement = document.getElementById("root");

// rootSaga 미들웨어 전달
sagaMiddleware.run(rootSaga);

render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);
  • redux-saga는 비동기 처리를 위해서 제너레이터를 사용한다.
    제너레이터와 비동기에 대한 설명은 우아한 테크러닝 4회차때 설명하셨다.
// sagas/index.ts
export default function* () {
  yield fork(monitoringWorkflow);
}
  • redux-saga/effects 패키지 밑에 있는 헬퍼함수들이다.
// sagas/index.ts
import { fork, all, take, race, delay, put } from "redux-saga/effects";
  • redux-saga/effects에서 불러오는 fork, all, take, race, delay, put들은 객체를 만든다.
//...
function* monitoringWorkflow() {
  while (true) {
    yield take(getType(Actions.startMonitoring));

    let loop = true;

    while (loop) {
      yield all([
        put({ type: getType(Actions.fetchSuccess) }),
        put({ type: getType(Actions.fetchFailure) })
      ]);

      const { stoped } = yield race({
        waitting: delay(200),
        stoped: take(getType(Actions.stopMonitoring))
      });

      if (stoped) {
        loop = false;
      }
    }
  }
}

export default function* () {
  // monitoringWorkflow 함수를 전달
  yield fork(monitoringWorkflow);
}
  • fork(monitoringWorkflow)console.log를 해보면 객체가 만들어지는 것을 확인할 수 있다.

  • put 함수는 액션을 dispatch하는 것을 redux-saga에게 시키는 것이고 all을 사용하면 여러개의 액션을 dispatch할 수 있다.
  • take는 리덕스의 액션을 주고(문자열) 이 문자열에 액션이 들어오면 알려달라고 next를 던져준다.
  • race는 여러개의 객체로 전달해도 상관 없고, 그 안에 객체 중에 제일 먼저 돌아오는 상태 값을 넘긴다.
  • delay는 Promise객체이고 특정 시간동안 기달리고 다시 실행되게 된다.
const { stoped } = yield race({
  waitting: delay(200),
  stoped: take(getType(Actions.stopMonitoring))
});

redux-saga의 자세한 내용은 공식문서 참고
번역본도 존재.

🚀 6회차 후기

벌써 다음주면 마지막 주차이다..😥 마지막 시간은 치킨 뜯으면서 해보자는데 진짜일까? ㅋㅋㅋ
3주동안 정말 리액트를 까보면서 구현해보니까 리액트에 대해서 더 잘 파악할 수 있는 계기가 된거같다.
이번주는 진짜? 리액트와 typescript를 사용하면서 구조를 이해해봤다. 또한, redux-saga에 대해서도 다시한번 더 알게된 시간이였다.
그리고 redux-toolkit은 다음번에 꼭 사용해보고 싶다.🚀