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

✌ 9월24일 (목) 우아한 테크러닝 3기 8회차 강의 정리
🚀 8회차 강의 목표
- 마지막 예제 살펴보기
- Mobx 살펴보기
🚀 예제 살펴보기
🌈 React-Router
- 이 예제는 1년전 예제라 react-router 공식 문서를 참고하며 보기를 바란다.
router의 진행 동작은 URL이 바뀌는 것을 내부의location의path를 감지해서 해당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컴포넌트는label만props로 전달해주고 있다.
export default class Order extends React.PureComponent<IProps> { render() { return ( <React.Fragment> <AuthContainer> <PageHeader label="주문" /> </AuthContainer> <OrderListContainer /> </React.Fragment> ); } }- 하지만
PageHeader컴포넌트를 확인해보면label이외에authentication,requestLogout,openNotificationCenter를props로 받고 있다.
export const PageHeader: React.FC<IProps> = ({ label, authentication, requestLogout, openNotificationCenter }) => { // ... 생략 return ( // ... 생략 ); };- 결국 직접적으로
props를 전달해주는 건AuthContainer가 대신해준다. AuthContainer.tsxmap을 하면서 받은props를cloneElement를 이용해서 주입을 시켜준다.
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: [] };- 예제의 자세한 내용과 코드는 김민태님의 CodeSandBox 참고
🚀 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을 사용한다.autorun는observable로 만든 객체가 자동으로 추적해서 변경이 되면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일 때는 어떻게 해야할까?Mobx의observable은 기본값이 객체이기 때문에 기본적으로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,weight3개의 값이 변경이 되기 때문에autorun3번씩 불러지게 된다. - 그렇기 때문에 이 변경 단위를
Mobx의action을 사용하여 묶어줄 수 있다.action은 논리적인 작업 단계를 묶어주는 것이다. (redux의action이랑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.json에experimentalDecorators를true로 추가 시켜주면 정상적으로 동작하는 것을 확인 할 수 있다.
{ "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 Hooks에useEffect와 비슷한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을 사용하는 것은 안되기 때문에observable을subscribe하는 Mobx-react의obsever를 구성할 수 있다. subscribe를 하기 때문에App컴포넌트의props를 제거해준다.
// index.tsx autorun(() => { render(<App />, rootElement); });- Mobx-react가 제공하는 것 중에
inject와observer가 있다.
// App.tsx import { inject, observer } from "mobx-react";observer는Primitive Type과function은 지원하지 않기 때문에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기를 끝났다. 😢
이번 테크러닝을 통해서 많이 배웠고 어떤 것을 공부해야할지 더 알게된 계기가 되었다.
그리고 끝날 때마다 블로깅하는 것도 조금은 귀찮았지만 블로깅하면서 한번 더 복습하는 계기가 되어서 역시 힘들지만 엄청 보람차다. 🚀
다음 번에 또 기회가 있어서 꼭 다시 듣고 싶다!
한달 동안 좋은 강의 해주신 김민태님 감사합니다!! 🙏'우아한 테크러닝 3기' 카테고리의 다른 글
✔ 우아한 테크러닝 3기: React & TypeScript 7회차 (0) 2020.09.23 ✔ 우아한 테크러닝 3기: React & TypeScript 6회차 (0) 2020.09.20 ✔ 우아한 테크러닝 3기: React & TypeScript 5회차 (4) 2020.09.19 ✔ 우아한 테크러닝 3기: React & TypeScript 4회차 (0) 2020.09.11 ✔ 우아한 테크러닝 3기: React & TypeScript 3회차 (0) 2020.09.09