-
✔ 우아한 테크러닝 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.tsx
map
을 하면서 받은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
,weight
3개의 값이 변경이 되기 때문에autorun
3번씩 불러지게 된다. - 그렇기 때문에 이 변경 단위를
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