ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ✔ 우아한 테크러닝 3기: React & TypeScript 8회차(마지막 회차)
    우아한 테크러닝 3기 2020. 9. 26. 02:07

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

    🚀 8회차 강의 목표

    🚀 예제 살펴보기

    🌈 React-Router

    • 이 예제는 1년전 예제라 react-router 공식 문서를 참고하며 보기를 바란다.
    • router의 진행 동작은 URL이 바뀌는 것을 내부의 locationpath를 감지해서 해당 location의 주소에 맞는 컴포넌트와 연결시켜주는 간단한 동작이다.
      그렇기 때문에 서버 사이드에서 말하는 router와 다르다. 서버 사이드에서는 요청 URI end point path에 따라서 다른 비지니스 로직을 실행하는 컴포넌트를 연결시키는 것이고 여기서의 router는 URL의 변경 사항과 그것과 mapping되는 페이지 컴포넌트들을 연결시켜주는 역할을 한다.
    import * as React from "react";
    import { BrowserRouter, Switch, Route } from "react-router-dom";
    import {
      DefaultLayout,
      FullSizeLayout,
      NotificationContainer
    } from "../containers";
    import * as Pages from "../pages";
    import PrivateRoute from "./PrivateRoute";
    
    interface IProps {
      children?: React.ReactNode;
    }
    
    const Router: React.FC<IProps> = () => {
      return (
        <BrowserRouter>
          <NotificationContainer />
          <Switch>
            // react-router-dom이 제공하는 컴포넌트
            <Route exact path="/login">
              <FullSizeLayout>
                <Pages.Login />
              </FullSizeLayout>
            </Route>
            <DefaultLayout>
              <Switch>
                // 인증정보들을 체크해서 안되어있을 때 다른 동작을 취하게 만드는 PrivateRoute
                // 로그인안하면 못들어가는 url 같은 것
                <PrivateRoute exact path="/" page={Pages.Dashboard} />
                <PrivateRoute exact path="/orders" page={Pages.Order} />
                <PrivateRoute exact path="/shops" page={Pages.Shops} />
                <Route component={Pages.PageNotFound} />
              </Switch>
            </DefaultLayout>
          </Switch>
        </BrowserRouter>
      );
    };
    
    export default Router;
    • 아래는 PrivateRoute 컴포넌트로 로그인이 필요한 컴포넌트로 이동할 때 로그인 되지 않았았을 때 redirect 시켜주는 로직이다.
    const PrivateRouter: React.FC<IProps & IStateToProps & RouteProps> = props => {
      const Page: RoutePageComponent = props.page;
      const { authentication } = props;
    
      return (
        <Route
          {...props}
          render={props => {
            if (authentication) {
              return <Page {...props} />;
            } else {
              return (
                <Redirect
                  to={{
                    pathname: "/login",
                    state: { from: props.location }
                  }}
                />
              );
            }
          }}
        >
          {props.children}
        </Route>
      );
    };
    • PrivateRouter에서 Route 컴포넌트를 보면 아래와 같은 형태를 render 패턴이라고 부른다.
      컴포넌트에 children을 사용하는 것이 아니라 render라고 하는 props를 제공해주고 만약에 rendering관련한 기본동작을 변경할려면 render props에다가 함수를 제공하는데 이 함수는 결국 react 컴포넌트이다.
    • authentication 존재하면 그냥 받은 props(페이지)를 렌더링해주고 만약 없으면 login 페이지로 redirect 해준다.
    <Route
      {...props}
      render={props => {
        if (authentication) {
          return <Page {...props} />;
        } else {
          return (
            <Redirect
              to={{
                pathname: "/login",
                state: { from: props.location }
              }}
            />
          );
        }
      }}
    >
      {props.children}
    </Route>

    🌈 컴포넌트의 아키텍처

    • 각 컴포넌트별 파일 이름을 잘 지어 주어야 한다.
      아래 사진과 같이 page 컴포넌트들의 이름을 Order.tsx 이런 식 보다는 OrderPage.tsx를 붙여서 명확하게 나타내는 방법이 더 좋은 방법이다.

    • 아래 Order.tsx를 살펴보면 PageHeader 컴포넌트는 labelprops 로 전달해주고 있다.

    export default class Order extends React.PureComponent<IProps> {
      render() {
        return (
          <React.Fragment>
            <AuthContainer>
              <PageHeader label="주문" />
            </AuthContainer>
            <OrderListContainer />
          </React.Fragment>
        );
      }
    }
    • 하지만 PageHeader 컴포넌트를 확인해보면 label이외에 authentication, requestLogout, openNotificationCenterprops로 받고 있다.
    export const PageHeader: React.FC<IProps> = ({
      label,
      authentication,
      requestLogout,
      openNotificationCenter
    }) => {
      // ... 생략
    
      return (
        // ... 생략
      );
    };
    • 결국 직접적으로 props를 전달해주는 건 AuthContainer가 대신해준다.
    • AuthContainer.tsx
      map을 하면서 받은 propscloneElement를 이용해서 주입을 시켜준다.
    const AuthWrapper: React.FC = props => {
      const children = React.Children.map(
        props.children,
        (child: React.ReactElement, index: number) => {
          return React.cloneElement(child, { ...props });
        }
      );
    
      return <React.Fragment>{children}</React.Fragment>;
    };
    
    export const AuthContainer = connect(
      // ... 생략
    )(AuthWrapper);
    • 이런 패턴의 장점은 PageHeader에서 반복되서 똑같이 내려가는 작업들을 숨김으로써 코드를 절약할 수 있는 장점이 생긴다.
    • 단점은 명시적으로 props가 받는게 보이지 않아서 직접 들어가서 확인해야 한다.
    • 비지니스 로직이 없고 단순 데이터만 주입시키는 형태에서 사용할 때 편리하다.
    export default class Order extends React.PureComponent<IProps> {
      render() {
        return (
          <React.Fragment>
            <AuthContainer>
              <PageHeader label="주문" />
            </AuthContainer>
            <OrderListContainer />
          </React.Fragment>
        );
      }
    }
    • 위 와 같은 형태에서 아래와 같이 변경이 가능하다.
    export default class Order extends React.PureComponent<IProps> {
      render() {
        return (
          <>
            <AuthContainer>
              <PageHeader label="주문" />
            </AuthContainer>
            <OrderListContainer />
          </>
        );
      }
    }
    • 또한 이 방법도 가능하다.
    export default class Order extends React.PureComponent<IProps> {
      render() {
        return <>
            <AuthContainer>
              <PageHeader label="주문" />
            </AuthContainer>
            <OrderListContainer />
          </>;
      }
    }
    • 아래는 store의 상태의 디자인이다.
      기본적으로 depth가 깊어지면 어려워지기 때문에 아래와 같이 depth를 유지하는 것이 편리하다.
    export const initializeState: IStoreState = {
      authentication: null,
      monitoring: false,
      shopList: [],
      openNotificationCenter: false,
      showTimeline: false,
      duration: 200,
      asyncTasks: [],
      notifications: [],
      success: 0,
      failure: 0,
      successTimeline: [],
      failureTimeline: []
    };

    🚀 Mobx 살펴보기

    • 증가하는 counter 예제로 살펴보자.
    • Mobx를 알아보기 전에 우리는 setInterval로 값을 증가시킬 수 있다.
    // App.tsx
    import * as React from "react";
    import "./styles.css";
    
    interface AppProps {
      data: number;
    }
    
    export default function App(props: AppProps) {
      return (
        <div className="App">
          <h1>외부 데이터: {props.data}</h1>
        </div>
      );
    }
    • index.tsx
    import * as React from "react";
    import { render } from "react-dom";
    
    import App from "./App";
    
    const store = {
      data: 1
    };
    
    const rootElement = document.getElementById("root");
    
    setInterval(() => {
      store.data++;
      render(<App data={store.data} />, rootElement);
    }, 1000);
    • 하지만 위와 같이 이렇게 할 수는 없다.
      위와 같은 예제는 데이터가 바꼈다라는 통지를 우리가 직접 setInterval사용하여 해준 것이다.
    • Mobx는 observable이라는 타입을 제공하고 observable은 함수이다. 원하는 데이터를 observable로 감싸준다.
    import { observable } from "mobx";
    // ...
    const cart = observable({
      data: 1
    });
    // ...
    • Mobx가 제공하는 redux의 subscribe와 같은 autorun을 사용한다.
      autorunobservable로 만든 객체가 자동으로 추적해서 변경이 되면 autorun에 등록된 함수를 실행시켜준다.
    import * as React from "react";
    import { observable, autorun } from "mobx";
    import { render } from "react-dom";
    import App from "./App";
    
    const cart = observable({
      data: 1
    });
    
    const rootElement = document.getElementById("root");
    
    autorun(() => {
      render(<App data={cart.data} />, rootElement);
    });
    
    setInterval(() => {
      cart.data++;
    },1000)
    • counter 상태를 추가시킨후 counter는 2씩 증가시켜보자.
    // index.tsx
    const cart = observable({
      data: 1,
      counter: 1
    });
    
    const rootElement = document.getElementById("root");
    
    autorun(() => {
      render(<App data={cart.data} counter={cart.counter} />, rootElement);
    });
    
    setInterval(() => {
      cart.data++;
      cart.counter+=2;
    }, 1000);
    
    // App.tsx
    interface AppProps {
      data: number;
      counter: number;
    }
    
    export default function App(props: AppProps) {
      return (
        <div className="App">
          <h1>
            외부 데이터: {props.data} vs. {props.counter}
          </h1>
        </div>
      );
    }
    • 만약 상태가 객체 형태가 아니라 Primitive type 일 때는 어떻게 해야할까?
      Mobxobservable은 기본값이 객체이기 때문에 기본적으로 Primitive type을 지원하지 않는다.
      그렇기 때문에 observable.box함수를 사용하면 Primitive type을 사용할 수 있다.
      하지만 Primitive type을 사용하는 경우는 별로 없다.
    const weight = observable.box(63);
    • 새로운 값을 넣을려면 set을 사용하고 값을 가져올려면 get을 사용한다.
    // 생략..
    autorun(() => {
      console.log(weight.get());
      render(<App data={cart.data} counter={cart.counter} />, rootElement);
    });
    
    setInterval(() => {
      cart.data++;
      cart.counter += 2;
      weight.set(weight.get() - 1);
    }, 1000);
    • 여기서 console.log를 찍어 확인해보면 이상한 점을 발견할 수 있다.
      아래와 같이 동일한 값이 3번씩 찍히는 것을 확인할 수 있다.

    </p|data-origin-width="201" data-origin-height="208" data-ke-mobilestyle="widthOrigin"|alignCenter||_##]

    • 이러한 이유는 observable된 값들 각각 마다 변경이 될 때 각각 하나씩 여러번 불리게 된다. 지금은 cart.data, cart.counter, weight 3개의 값이 변경이 되기 때문에 autorun 3번씩 불러지게 된다.
    • 그렇기 때문에 이 변경 단위를 Mobxaction을 사용하여 묶어줄 수 있다.
      action은 논리적인 작업 단계를 묶어주는 것이다. (reduxaction이랑 dispatch하는 것과 비슷하다.)
    import { observable, autorun, action } from "mobx";
    
    // 생략..
    const myAction = action(() => {
      cart.data++;
      cart.counter += 2;
      weight.set(weight.get() - 1);
    });
    
    // 생략..
    setInterval(() => {
      myAction();
    }, 1000);
    • 위 예제는 action 따로 사용하는데 cart 객체에 class로 변경하여 action을 같이 사용할 수 있다.
    class Cart {
      data: number = 1;
      counter: number = 1;
    
      myAction = action(() => {
        this.data++;
        this.counter += 2;
      });
    }
    
    // 인스턴스 객체를 생성한다.
    const cart = new Cart();
    
    // 생략...
    setInterval(() => {
      cart.myAction();
    }, 1000);
    • 어노테이션을 사용하여 observable을 사용할 수 있다.
    class Cart {
      @observable data: number = 1;
      @observable counter: number = 1;
    
      myAction = action(() => {
        this.data++;
        this.counter += 2;
      });
    }
    • 하지만 어노테이션은 typescript에서도 실험적인 기능이기 때문에 tsconfig.jsonexperimentalDecoratorstrue로 추가 시켜주면 정상적으로 동작하는 것을 확인 할 수 있다.
    {
        "include": [
            "./src/*"
        ],
        "compilerOptions": {
            "strict": true,
            "esModuleInterop": true,
            "experimentalDecorators": true,
            "lib": [
                "dom",
                "es2015"
            ],
            "jsx": "react"
        }
    }
    • action 도 어노테이션을 사용하여 바꾸어주자.
    class Cart {
      @observable data: number = 1;
      @observable counter: number = 1;
    
      @action
      myAction = () => {
        this.data++;
        this.counter += 2;
      }
    }
    • autorun 뿐만 아니라 조건에 맞게 사용 가능한 when도 존재한다.
    class MyResource {
        constructor() {
            when(
                // once...
                () => !this.isVisible,
                // ... then
                () => this.dispose()
            )
        }
    
        @computed get isVisible() {
            // indicate whether this item is visible
        }
    
        dispose() {
            // dispose
        }
    }
    • react HooksuseEffect와 비슷한 reaction을 사용한다.
      reaction은 두 개의 함수를 받는데 앞에 함수의 return 값이 두 번째 함수 리턴으로 들어가서 두 번째 함수는 앞에 값이 변경되었을 때만 동작하도록 할 수 있다.
    // correct use of reaction: reacts to length and title changes
    const reaction2 = reaction(
        () => todos.map((todo) => todo.title),
        (titles) => console.log("reaction 2:", titles.join(", "))
    )

    📚 Mobx-react

    • 계속 autorun을 사용하는 것은 안되기 때문에 observablesubscribe하는 Mobx-reactobsever를 구성할 수 있다.
    • subscribe를 하기 때문에 App컴포넌트의 props를 제거해준다.
    // index.tsx
    autorun(() => {
      render(<App />, rootElement);
    });
    • Mobx-react가 제공하는 것 중에 injectobserver가 있다.
    // App.tsx
    import { inject, observer } from "mobx-react";
    • observerPrimitive Typefunction은 지원하지 않기 때문에 App 컴포넌트를 class 컴포넌트로 변경해주어야 한다.
    // 생략..
    @observer
    export default class App extends React.Component<AppProps> {
      render() {
        return (
          <div className="App">
            <h1>
              외부 데이터: {this.props.data} vs. {this.props.counter}
            </h1>
          </div>
        );
      }
    }
    • 그 후 실제로 데이터를 받아올 때는 inject 어노테이션을 사용해서 주입할 이름을 적어주면 this.props로 자동으로 주입된다.
    @inject("cart")
    @observer
    export default class App extends React.Component<AppProps> {
      render() {
        return (
          <div className="App">
            <h1>
              외부 데이터: {this.props.data} vs. {this.props.counter}
            </h1>
          </div>
        );
      }
    }
    • 하지만 inject 어노테이션을 typescript에서 사용하면 단점은 필수가 아니라 optional로 해줘야한다.
    interface AppProps {
      data?: number;
      counter?: number;
    }

    🚀 마지막 주차(8회차) 후기

    한달 간의 우아한 테크러닝 3기를 끝났다. 😢
    이번 테크러닝을 통해서 많이 배웠고 어떤 것을 공부해야할지 더 알게된 계기가 되었다.
    그리고 끝날 때마다 블로깅하는 것도 조금은 귀찮았지만 블로깅하면서 한번 더 복습하는 계기가 되어서 역시 힘들지만 엄청 보람차다. 🚀
    다음 번에 또 기회가 있어서 꼭 다시 듣고 싶다!
    한달 동안 좋은 강의 해주신 김민태님 감사합니다!! 🙏

    댓글

Designed by Seungmin Sa.