우아한 테크러닝 3기

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

seungmin2 2020. 9. 23. 22:42

✌ 9월22일 (화) 우아한 테크러닝 3기 7회차 강의 정리

🚀 7회차 강의 목표

  • React 에서 컴포넌트 분리
  • React & TypeScript 예제 훑어보기

🚀 컴포넌트는 어떻게 분할할까?

  • 컴포넌트가 외부 상태에 의존적인 것과 외부 상태와 의존적인 컴포넌트를 분리하는 하는 것은 react의 원칙적인 것이다.
  • 여기서 말하는 외부 상태에 의존적인 것은 비지니스 로직을 의미한다.
  • 비지니스 로직은 상태를 변경하면서 어떤 상태를 어디로부터 어떤것을 가지고 올지와 관련된 코드들을 말한다.
    그렇기 때문에 비지니스 로직을 가지고 있는 컴포넌트들은 여러가지 단계로 분리하는데 개별 컴포넌트들한테 데이터를 전달하는 코드들은 container 컴포넌트라고 불리고 이것들을 아래와 같이 containers 폴더에 따로 분리해준다.

  • props만 받아와 사용하는 컴포넌트들은 비지니스 로직이 없는 컴포넌트로 상태가 존재하지 않다.
    그런 컴포넌트들은 위와 같이 components폴더에 분리하여 작성해준다.
  • 이 컴포넌트들은 자체적으로 가지고 있는 상태가 있을 수가 있다. (예를 들어 Hooks)
  • 하지만 중요한 것은 내부적으로 상태를 가지고 있건 말건 그것은 독립적인지 아니면 외부와 커뮤니케이션이 필요한지에 따라서 달라질 수 있다.

📌 그렇다면 어느 시점에 분리를 할까??
코드를 쓰다보면 고민되는 시점이 있다. 그때가 분리해야 하는 시점이다?!
너무 처음부터 자기가 알고있는 작은 단위의 패턴으로 컴포넌트를 분리하면 그 크기가 안됬을 때는 굉장히 피곤하고, 그 볼륨보다는 아키텍처에만 집중하게 되는 상황이 나오게 된다.

🚀 React & TS Boilerplate 예제

  • App 컴포넌트에서는 아래와 같이 3개의 Container 컴포넌트를 가지고 있다.

    NotificationContainer, OrderStatusContiner, MonitorControllerContainer

import * as React from "react";

import {
  NotificationContainer,
  OrderStatusContiner,
  MonitorControllerContainer
} from "./containers";
import { Typography } from "antd";

import "antd/dist/antd.css";
import "./sass/main.scss";

export default class App extends React.PureComponent {
  render() {
    return (
      <div>
        <NotificationContainer />
        <header>
          <Typography.Title>React & TS Boilerplate</Typography.Title>
        </header>
        <main>
          <OrderStatusContiner />
          <MonitorControllerContainer />
        </main>
      </div>
    );
  }
}
  • 그리고 containers에는 index.ts파일을 가지고 있고, components 폴더에도 index.ts 파일을 가지고 있다.
// ../containers/index.ts
export * from "./OrderStatus";
export * from "./MonitorController";
export * from "./Notification";
  • 위와 같이 index.ts를 구성하여 사용하면 바깥에서 사용할 때 사용할 컴포넌트를 간단하게 사용할 수 있는 장점을 가진다.
    또한, 내부의 정보를 외부에서 감출 수가 있다. (캡슐화)
import {
  NotificationContainer,
  OrderStatusContiner,
  MonitorControllerContainer
} from "./containers";

🌈 React에서 TypeScript

  • 컴포넌트의 props의 타입을 지정하는 경우에는 아래와 같은 형식으로 많이 작성된다.
// OrderStatus.tsx
export interface OrderStatusProps {
    showTimeline: boolean;
    success: number;
    failure: number;
    successTimeline: ITimelineItem[];
    failureTimeline: ITimelineItem[];
}

class OrderStatus extends React.Component<OrderStatusProps> {
// ...
}
  • 위에서 interface에서 논쟁이 있다.

    interfacetypealias중에서 무엇을 사용할까?? 🤔

📚 Interface와 Type Alias

  • TypeScript에서 type을 기술하는 두 가지 방법으로 InterfaceType Alias로 사용할 수 있다.
type Person = {
    name: string;
    age: number;
    job?: [];
};

interface Human {
    name: string;
    age: number;
    job?: [];
}
  • 이 둘은 컴파일 타임에 적용되는 스펙이다.
  • typescript playground에서 트랜스 파일링한 결과는 아무것도 나오지 않는다.

  • 아래의 코드는 사용자가 어떤 값을 입력할지 예측할 수 없다.
    또한, 이 타입(Person)이라고 하는 것은 결국 JavaScript로 변환할 때 작동하는 메커니즘이기 때문에 런타임에 타입을 검사하는 것은 존재하지 않는다.
    그렇기 때문에 TypeScript로 작성했다고 해서 모든 타입에러를 잡는 것은 아니고, 어떤 것이 런타임인지 컴파일 타임인지를 명확하게 알고 있지 않으면 어떤 상황이 버그로 예측하기 힘들다.
type Person = {
    name: string;
    age: number;
    job?: [];
};

const p: Person = JSON.parse(prompt("객체를 입력해주세요"));
  • 그래서 위 코드는 오류 처리가 필요하다.
  • Interface와 Type Alias의 두개는 거의 기능적으로는 유사하다.
  • Interface는 실제로 상속을 지원하지만 유니온 타입은 지원하지 않는다.
  • Type Alias는 유니온 타입을 지원한다.
type box = number | string;
let b: box = 1;
b = "box";
  • 현재 대세적으로는 type을 많이 사용한다.

🤔 React Component의 class OOP(객체 지향 프로그래밍)을 따를까?

class OrderStatus extends React.Component<OrderStatusProps> {
  // 생략..
  render() {
    // 생략..
  }
}

export const OrderStatusContiner = connect(mapStateToProps)(OrderStatus);
  • react 컴포넌트의 classOOP적인 성격은 가지고 있지 않다.
  • react classOOP를 구성하기 위해서 만든 것이 아니다.
  • 컴포넌트 Life Cycle를 만들기 위한 도구에 지나지 않는다.
    그래서 react로 프로그래밍을 할 때 컴포넌트를 만들기 위한 class는 단 한번도 new를 하지않는다. (인스턴스를 만들지 않는다.)
    react가 다 알아서 하기 때문에 class안에 있는 것들은 전부다 public method일 수 밖에 없다.(밖으로 다 들어나야되기 때문)
  • 심지어 요즘에는 함수형 컴포넌트를 지원하기 때문에 더욱 더 react로 만든 앱에 oop성격은 굉장히 약화되어있다.
    그런 측면에서 typescript 생각해보면 그렇게 많이 기능을 쓸것은 없다. 특히 reduxsaga의 조합이면 더 더욱 쓸 것은 줄어든다.

📚 TypeScript에서 제네릭이란?

  • 제네릭 공식문서 참고
  • 아래와 같이 any로 모든 타입을 지정할 수 있다.(좋지 않다.)
function identity(arg: any): any {
  return arg;
}
  • 아래의 T라고 하는 것을 규정을 하고 argT라고 하는 것을 호출되는 시점에 결정이 된다.
    만약에 TString 타입으로 규정하면 argString으로 넘겨주고 String으로 리턴되게 된다.
    이러한 동적타입을 제네릭이라고 한다.
function identity<T>(arg: T): T {
  return arg;
}

let output = identity<string>("myString");
//       ^ = let output: string
  • Typescript의 제네릭은 컴파일 타임에 동작하는 코드이다.
    TypeScript playground에서 확인하면 아무것도 결과에 나오지가 않는다. (자바스크립트관점에서)
  • 리액트 컴포넌트의 제네릭은 아래와 같이 사용한다.
class OrderStatus extends React.Component<OrderStatusProps> {
  // 생략..
  render() {
    // 생략..
  }
}
  • P는 컴포넌트의 props, S는 컴포넌트 내부의 state를 규정하고 SSsnapshot(잘 사용하지 않는다.)을 의미한다.
class React.Component<P = {}, S = {}, SS = any>

📚 react-redux를 사용하여 redux와 컴포넌트 연결하기

  • 옛날 버전의 react-redux는 컴포넌트와 redux를 연결하기 위해 connect 헬퍼 함수를 사용했다.
  • connect 함수는 HoC(High order Component)(함수를 받고 함수를 리턴) 형태로 작성되어 있다.
  • connect는 호출을 두 번 한다.
  • connectreturn하는 함수*connect가 최초 호출할 때 받는 mapStateToProps 객체가 하는 일을 클로저로 가지고 있다가 connect의 두번째 OrderStatus 함수한테 property로 주입시키는 역할을 한다.
    OrderStatus라는 함수는 store에 구독받는 형식으로 바뀌게 되고 그 바뀐 형태의 컴포넌트를 export 해주는 것이다.
// ...
import { connect } from "react-redux";
// ...

// StoreState에서 필요한것만 뽑아서 connect함수로 주면 OrderStatus로 주입을 해준다.
const mapStateToProps = (state: StoreState) => {
  return {
    showTimeline: state.showTimeline,
    success: state.success,
    failure: state.failure,
    successTimeline: state.successTimeline,
    failureTimeline: state.failureTimeline
  };
};

class OrderStatus extends React.Component<OrderStatusProps> {
 // ... 생략
}

export const OrderStatusContiner = connect(mapStateToProps)(OrderStatus);
  • 지금은 react-redux에서도 함수형 컴포넌트를 사용하기 위해 Hooks 지원해서 쉽게 사용이 가능하다.
  • 아래와 같이 useSelector를 사용하면 store에 있는 데이터를 가져올 수 있다.
import React from 'react'
import { useSelector } from 'react-redux'

export const CounterComponent = () => {
  // 함수를 전달
  const counter = useSelector(state => state.counter)
  return <div>{counter}</div>
}
  • useDispatch 사용하면 언제든지 디스패치를 꺼내서 사용할 수 있다.
import React from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()

  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch({ type: 'increment-counter' })}>
        Increment counter
      </button>
    </div>
  )
}
  • useSelectoruseDispatch를 사용하므로써 간단하게 connect함수를 대신하여 store에 있는 데이터를 가져올 수 있다.

📚 axios를 사용한 API 통신

  • API 메시지 response에 대한 규격을 interface를 하고 있다.
import axios, { AxiosResponse, AxiosError } from "axios";
import endpoint from "./endpoint.config";

// 성공
interface IApiSuccessMessage {
  status: string;
}

// 에러
interface IApiError {
  status: string;
  statusCode: number;
  errorMessage: string;
}

export class ApiError implements IApiError {
  status: string = "";
  statusCode: number = 0;
  errorMessage: string = "";

  constructor(err: AxiosError) {
    this.status = err.response.data.status;
    this.statusCode = err.response.status;
    this.errorMessage = err.response.data.errorMessage;
  }
}

interface INumberOfSuccessfulOrderResponse extends IApiSuccessMessage {
  result: {
    success: number;
  };
}

interface INumberOfFailedOrderResponse extends IApiSuccessMessage {
  result: {
    failure: number;
  };
}

interface IOrderTimelineResponse extends IApiSuccessMessage {
  results: {
    successTimeline: [];
    failureTimeline: [];
  };
}
  • 실제로 각각 사용하기에는 불편하기 때문에 아래와 같이 함수로 Promis 형태로 작성했다.
export function fetchNumberOfSuccessfulOrder(): Promise<
  INumberOfSuccessfulOrderResponse
> {
  return new Promise((resolve, reject) => {
    // axios 랩핑한 것
    axios
      .get(endpoint.orders.request.success({ error: true }))
      .then((resp: AxiosResponse) => resolve(resp.data))
      .catch((err: AxiosError) => reject(new ApiError(err)));
  });
}

export function fetchNumberOfFailedOrder(): Promise<
  INumberOfFailedOrderResponse
> {
  return new Promise((resolve, reject) => {
    // axios 랩핑한 것
    axios
      .get(endpoint.orders.request.failure())
      .then((resp: AxiosResponse) => resolve(resp.data))
      .catch((err: AxiosError) => reject(new ApiError(err)));
  });
}

export function fetchOrderTimeline(
  date: string
): Promise<IOrderTimelineResponse> {
  return new Promise((resolve, reject) => {
    axios
      .get(endpoint.orders.request.timeline(date))
      .then((resp: AxiosResponse) => resolve(resp.data))
      .catch((err: AxiosError) => reject(new ApiError(err)));
  });
}
  • 아래는 실제로 axios.get을 하면 endpoint(URI)를 빌드하는 것이다.
  • API가 많아지면 여기에 생기게 된다.
  • 반복적으로 처리하지 않기 위해 따로 분리하여 아래와 같이 처리한다.
interface Config {
  orders: {
    request: {
      success(options: { error?: boolean }): string;
      failure(): string;
      timeline(date: string): string;
    };
  };
}

// process.env.production 분기!!
const config: Config = {
  orders: {
    request: {
      success: ({ error = false }) =>
        `${SERVER}/${API_PREFIX}/orders/request/success${
          error ? "?error=random" : ""
        }`,
      failure: () => `${SERVER}/${API_PREFIX}/orders/request/failure`,
      timeline: date => `${SERVER}/${API_PREFIX}/orders/request/all/${date}`
    }
  }
};

export default config;

🚀 7회차 후기

이번시간은 자세히는 아니지만 typescript를 사용하여 react를 작성하는 것을 배웠다.
typescript와 더 친해지고 싶다... 공부가 많이 필요할 듯하다.
이제 1회차면 1달간 진행했던 우아한 테크러닝 3기도 마무리가 된다. 😂
마지막까지 잘듣고 블로깅해야겠다. 🚀