우아한 테크러닝 3기 React + TS

yaeji120·2020년 9월 24일
0

우아한 테크러닝

목록 보기
8/8
  • 8일차 마지막날 (2020.09.24.목)
    --

Router

React-router
어떤 URL이 바뀌면 그 주소랑 맞는 Component를 연결시켜주는 일을 하는 Component다.
URL의 변경사항과 그것과 연결되어있는 Page Component들을 연결시켜주는 역할을 한다.
크게 보면 '#'으로 시작하는 해시를 쓰는 방법이 있고 그냥 일반 URI를 쓰는 방법이 있는데
요즘에는 대부분 일반 URI를 많이 쓴다.
예전에는 하이브라우저에서 안되는 부분들이 있어서 그런 브라우저에서는 해시로 작동을 했는데
요즘은 그런 브라우저가 많이 없고 하이브라우저 지원도 많이 올라갔기 때문이다.

Server Side
React-router에서 말하는 Router랑은 조금 다른데
요청 URI end-point에 따라서 다른 비즈니스 로직을 실행하는 Component를 연결시키는 역할을 한다.

Route Component
react-router-dom이 기본적으로 제공해준다.
이걸 한번 감싸서 PrivateRoute Component로 만들어
인증정보나 특정한 정보들을 체크해서 다른 동작들을 할 수 있도록 작성할 수 있다.
예를 들어 로그인을 안하면 접속할 수 없는 URL이라고 생각하면 된다.

import { BrowserRouter, Switch, Route } from "react-router-dom";
import PrivateRoute from "./PrivateRoute";

const Router = () => {
  <BrowserRouter>
    <Switch>
      <PrivateRoute />
      <Route />
    </Switch>
  </BrowserRouter>
}

MobX

Redux와 마찬가지로 React와 상관없이 쓸 수 있는 일반 라이브러리지만
기능이 엄청 많고 배워야할 것도 많다.
그래서 React에서 잘 사용할 수 있도록 나온게 mobx-react다.

‣ Observable, Autorun

Redux와 다르게 MobX는 상태를 개별 개별로 여러개 만들 수 있다.
Redux에서는 subscribe가 render를 감싸고 이를 통해 바뀐 상태를 통지해
바뀌었을때만 render를 호출하도록 되어있다.
MobX에서는 observable이라는 함수를 제공해준다.
내가 원하는 객체를 observable이라는 함수에게 넘겨주면 된다.

import { observable } from "mobx";

const store1 = observable({
  data: 1
});

또한 autorun이라는 것도 제공해주는데 Redux의 subscribe같은 것이다.
아래의 예제에서 autorun에 콘솔 로그를 찍는 함수를 넘겼는데 콘솔창에 두개가 찍히는 이유는
첫 번째 콘솔은 최초의 autorun이 등록할 때 실행되면서 찍힌것이고
두 번째 콘솔은 18번째 줄에서 observable안에 있는 객체의 data의 값을 변경했기 때문에
그 변경 사항을 자동으로 추적해서 바뀌면 autorun에 있는 함수를 실행해주기 때문에 찍힌것이다.

subscribe는 무조건 reducer가 작동하면 실행되고
autorun은 observable안에 있는 데이터의 변경이 autorun함수 안에서 쓰이면 그것만 실행해준다.
이렇듯 무언가를 배울 때 내가 잘 알고 있는 것에 빗대어 보면 조금 더 금방 익숙해진다.

위의 예제를 아래와 같이 바꾸면 바깥쪽에서 데이터가 바뀌었을 때 render가 다시 호출되게 만들수 있다.

근데 이게 정말 제대로 작동이 된건지,
혹시 autorun이 비동기라 'cart.data++' 가 먼저 실행되고 autorun이 실행된건 아닌지
충분히 의심을 해 볼 만한 상황이다.
개발자들에게 이런 의심은 좋은 것이다.
확인을 위해 setInterval를 이용해 'cart.data++'를 감싸주면
오른쪽에 외부 데이터의 숫자가 증가하는 것을 볼 수 있다.

‣ Action

counter라는 새로운 상태를 추가했다.

기본적으로 observable로 감쌀 수 있는 것들은 객체지만 다른 타입도 넣을 수는 있다.

const weight = observable(100);

하지만 위와 같이 작성하면 에러가 난다.
observable의 기본값은 객체이기 때문에 number, boolean, string, function 등은 바로 넣을 수 없고
box로 한번 감싸줘야 한다.
이런게 있지만 보통 값 하나만 받으려고 이렇게 사용하는 경우는 거의 없다.
Observable한 값이 바뀔 때 변경 추적이 개별 개별 값대로 되는것에 대한 이해도를 높이기 위해 사용한 것이다.

const weight = observable.box(100);

아래의 예제의 23번째 줄과 같이 observable에서 제공해주는 set, get을 사용할 수 있다.

근데 위의 예제 오른쪽에 보이듯
autorun안에서 콘솔에 weight.get을 찍어보면 숫자가 3개씩 찍히는데
21, 22, 23번째 줄에서 모두 값의 변경이 일어나기 때문이다.
이를 통해 각각의 변경에 따라 autorun이 여러 번 불려진다는 것을 알 수 있다.
이런 문제를 해결하기 위해 변경의 단위를 묶어서 사용자가 원하는대로 정할 수 있도록
action이라는 헬퍼를 제공해 준다.

MobX action은 논리적인 작업단계를 묶어 일을 하는데 Redux의 action dispatch와 비슷하다.
Redux의 dispatch역시 데이터를 주고 할 일을 하나의 action의 묶음단위로 하는데
차이점은 그걸 reducer에서 처리한다는 것뿐이다.
MobX action은 reducer가 필요없는데
observable을 여러개로 나눠서 즉, 객체를 이용해 개별 단위로 사용하기 때문이다.

‣ class로 작성해보기

class로 작성할때는 Annotation을 사용한다.

@observable

Annotation을 사용하기 위해서는 tsconfig.json 파일 컴파일 옵션에 아래내용을 추가해야 한다.

"experimentalDecorators": true,

class 바로 위에 작성할 수도 있다.

@observable
class Cart {
  .
  .
  .
}

Action도 Annotation을 사용해 만들면 된다.

class Cart {
  @observable data: number = 1;
  @observable counter: number = 1;

  @action
  myAction = () => {
    this.data++;
    this.counter += 2;
  };
}

‣ When, Reaction

autorun은 구독 기능도 하면서 사이드이펙트를 작업을 취급하는 용으로 만든
여러 헬퍼 함수들 중 하나다.
공식문서에 보면 autorun, when, reaction 등이 있다.

autorun은 그냥 상태가 바뀌면 autorun 내부의 함수가 실행되는 것이다.

when은 어떨땐 뭐를 한다는 조건을 걸 수 있다.

reaction은 React Hook의 useEffect랑 비슷하다.
두 개의 함수를 받는데 앞의 함수 return값이 두번째 함수로 들어가는 구조여서
두번째 함수는 앞에서 값이 바뀌었을때만 동작한다.

‣ Singleton Pattern

MobX에서는 Singleton Pattern을 굉장히 많이 쓴다.
Singleton이란 한번 생성이 되면 아무리 다시 생성하려고 해도 다시 생성되지 않는
단 하나의 인스턴스 객체를 뜻한다.
인스터스를 새로 만들면 똑같은게 두 개가 생겨버리기 때문에 사용하지만
무조건 Singleton Pattern을 써야하는건 아니다.


MobX-React

버전 5점대까지는 React Hook을 지원하지 못한다.
그래서 Hook을 지원하는 MobX-React가 따로 있는데 MobX-React-Lite다.
혹시 예전부터 MobX로 프로젝트를 해오던 상황이라면 Hook을 지원하지 않을 수 있기 때문에
MobX-React-Lite로 Migration 하거나 MobX 버전을 6점대로 올려야한다.

React Component가 구독을 할 때 autorun으로 구독시키지는 않고
MobX-React에서 제공하는 Inject, Observer를 사용한다.

import { inject, observer } from "mobx-react";

Observer는 함수를 받을 수 없기 때문에
기존의 MobX를 쓰던 프로젝트의 Observer Component는 거의 전부 Class로 되어있다.
버전 6점대부터 React Hook을 지원하게 되면서 Class Component를
모두 Functional Component로 바꿔야하는 상황이라 복잡해진 부분도 있다.

‣ 비동기 작업

각자 보통 Class안에서 자기 도메인의 비동기 처리를 직접 한다.

mobx.configure({ enforceActions: "observed" })

class Store{
  @observable githubProjects = []
  @observable state = "pending" // "pending" / "done" / "error"
  
  @action
  fetchProjects(){
    this.githubProjects = []
    this.state = "pending"
    fetchGithubProjectsSomehow().then(
      (projects) => {
        const filterdProjects = somePreprocessing(projects)
        this.githubProjects = filteredProjects
        this.state = "done"
      },
      (error) => {
        this.state = "error"
      }
    )
  }
}

flow를 통해서 async action을 다룬다.
folw자체가 Action Annotation을 해주기 때문에
fetchProjects에 따로 Annotation을 붙이지는 않는다.
아래의 코드를 보면 flow가 작은 Redux-saga처럼 보인다.

mobx.configure({ enforceActions: "observed" })

class Store {
  @observable githubProjects = []
  @observable state = "pending"
  
  fetchProjects = flow(function* () {
    this.githubProjects = []
    this.state = "pending"
    try {
      const projects = yield fetchGithubProjectsSomehow()
      const filteredProjects = somePreprocessing(projects)
      this.state = "done"
      this.githubProjects = filteredProjects
    }
    catch (error) {
      this.state = "error"
    }
  })
}

observable도 하나하나 다 읽어보면 좋긴하지만 뭐가 되게 많다는 느낌이 들 수 있다.
큰 틀에서 보면 Observer가 있고 그걸 구독하는 Observer가 있다.
그 구독의 단위를 그룹으로 묶어주는 Action이 있다고 이해하면 MobX의 구성은 끝이다.
거기서 이제 디테일하게 Observer의 가지치기, Action의 가지치기를 하는 것이다.

Middleware같은 구성이 없다보니까 도메인 객체 기준으로 보면 로직이 더 잘 보이지만
서로 다른 도메인들끼리의 관심사를 처리하는 게 까다로워서 어렵고 역량이 필요하다.
그래서 도메인 설계를 잘 해야 한다.


Test

요즘 대세는 testing library다.
크게 보면 End to End , Unit Test가 있다.

테스트는 성공한 것과 실패한 것이 있는데
성공하는 테스트는 반드시 성공해야하고, 실패하는 테스트는 반드시 실패해야한다.
즉, 테스트 자체가 Funcional 하다는 것이다.
내가 어떤 Component를 만들었는데
이 Component가 이렇게 동작시켰을 때 이렇게 되야하는 건 성공 케이스,
이렇게 동작시켰을 때는 동작을 안해야되는 건 실패 케이스다.
성공하는 건 항상 성공하게 만들고
실패하는 건 항상 실패하게 만들어야한다는 점이 마치 순수함수같다.

테스트가 이런 속성을 가지고 있어서 사이드이펙트에서 격리되어야한다.
내가 테스트하는 대상이 Component, Class, Reducer, Middleware 등등 뭐든간에
주변 Dependency가 있어 그거에 따라 테스트가 성공하고 실패하면 안된다.
이런 관점을 가지고 있으면 테스트 대상 Component의 Dependency를 끊어야겠다,
어떻게 구성을 바꿔야 끊을수있을지를 생각해보게 되고
테스트로서 리팩토링이 가능하다.

테스터블하다는 것은 내가 테스트하는 대상끼리의 Dependency를 관리하는 것이다.
당연히 Dependency가 없어야 테스터블하고 테스트가 용이하다.
그럼 자연스럽게 앱의 구조, Component의 구조, 테스트 대상의 구조도 계속 바뀔 수밖에 없다.
테스트를 작성하면 앱의 구조가 좋아진다기보다는 테스터블하게된다가 더 정확한 표현이다.
테스터블하다는 것은 그 테스트 대상끼리의 Dependency, 종속성이 옅어진다는 것이다.
소프트웨어를 만들때 규모가 커지면서 복잡도가 올라가는 것은 Dependency 때문이다.
Dependency가 없다면 아무리 규모가 커지더라도 복잡도는 유지될 수 있다.
이런 맥락에서 보면 테스트가 왜 중요한지 알 수 있고
테스트가 왜 소프트웨어의 구조를 좋은 방향으로 이끌어가는지를 알 수 있다.

소프트웨어의 구조들을 테스터블하게 바꿔나가야하고,
API, UI, 사용자 Event 등 사이드이펙트가 있는 작업들은 모두 다 dummy data를 만들어야한다.
예를 들어 real API는 상황에 따라 성공할수도 실패할수도 있기 때문에 테스트가 아니다.
테스트는 성공하는 것은 무조건 성공해야하고 실패하는건 무조건 실패해야하기 때문에
real API로는 재현이 불가능하다.
테스트는 꼭 써야하는 것이지만 그 테스트시트를 유지하는데도 엄청난 비용이 들기때문에
이런부분도 잘 고민해봐야한다.


자료 링크

React Router

소스 코드

김민태님 CodeSandbox

우아한 형제들 - React에서 Mobx 경험기 (Redux와 비교기)

MobX

코드스피츠 - CSS Rendering

PageSpeed Insights


Q & A

Q1. 이직 준비 과정에서 이전 회사에서 작업하고 기여한 코드 공개가 불가능해
이력서나 포폴에 코드를 첨부하는 일이 어려울 경우에 어떻게 하는게 좋을지

  • 이직을 할 때 다녔던 회사의 코드를 첨부하는 경우는 없다.
    어떤 프로젝트를 했고, 어떤 문제가 있었는데 어떻게 해결했고 등과 같이
    본인이 했던 것들을 기술하면된다.
    보안규정이나 다른 이슈들이 있을 수 있기 때문에 첨부할 수 있어도 하면 안된다.

Q2. Redux로 UI 상태와 백엔드 데이터 모두 관리할 때
Redux-saga로 비즈니스 로직관리가 가능할듯한데 Container를 사용할 필요가 있는지

  • Redux-saga로 Container종류의 Component에 있는 비즈니스 로직을 모두 대체하기는 힘들다.
    디테일한 상태나 각 Container별로 내부에 필요한 로직들이 있을 수 있기 때문에
    그걸 모두 saga에 모아놓고 사용하는것은 불가능해 Container Component는 필요하다.

Q3. 경력으로 이직할 때 이전 회사에서 한 일을 어떤식으로 증명하는지
본인이 개발한 기능과 구조를 어떤식으로 구현했는지 설명하는 것만으로 실력을 알 수 있는지

  • 보통은 한 일을 얘기하면 면접관이 질문을 하고 그에 대한 대답을 통해 증명이 된다.

Q4. 기존 소스코드에 새로운 기술을 적용시킬 때 어떤식으로 해야 좋을지

  • TS라던가 Hook들은 부분 적용할 수 있도록 되어있기 때문에 조금씩 적용 범위를 늘려가기.
    사이드 이펙트가 적은 Component나 module들부터 시작해서 점진적으로 적용시켜보면 된다.

Q5. React Life Cycle

  • Life Cycle이라는 기본 컨셉은 아주 간단하다.
    최초에 만들어지고 업데이트가 되고 더 이상 사용되지 않아서 삭제되는 흐름이다.
    React Component 입장에서 업데이트가 된다는 건 render가 호출돼 화면이 재작성되는 것이다.
    state나 props 모두 데이터고 데이터가 바뀌면 리랜더링이 돼야 화면에 바뀐 데이터가 반영이 된다.
    랜더링은 render가 호출되거나 어떤 함수의 JSX가 return되면서 되는데
    실제 DOM에 반영되는 시점은 이때가 아니다.
    React가 적절한 시점에 DOM에 반영하는데 이게 매번 똑같을 수 없고 확정할 수 없기 때문에
    특정 Life Cycle Method를 제공해주는 것이다.

실제 마운트가 되는 시점이 언제인지는 정확히 알 수 없고
마운트와 관련된 Life Cycle Method들이 호출되면 마운트가 됐구나라고 생각할 수 있다.
Event Handler도 마찬가지다.
어떤 Click Event Handler가 있을 때 그 Event Handler가 호출됐다고 해서
언제 Click이 발생했는지까지는 정확히 알 수가 없다.

Q6. Validation을 진행한다고하면 Redux의 Middleware에서 수행하는지

  • Redux의 Middleware에서도 가능할 것 같다.
    action이 넘어오고 중간에서 그걸 가로채 데이터를 검증하는 벨리데이터를 통과시켜서
    문제가 있으면 뭔가를 할 수도 있다.
    어떤 종류의 Validation을 하느냐에 따라 좀 다르긴하지만
    Validation은 Middleware에서도, Reducer에서도, Component에서도 할 수 있다.
    하지만 대부분의 Validation은 사용자 피드백을 수반하기 때문에
    Component를 벗어나면 코드가 꼬이거나 복잡도가 높아지는 등 여러가지 어려운 점이 생긴다.
    또한 이런 대부분의 비즈니스 로직들은 Container에서 일어나기 때문에
    Validation 로직을 Component에서 작성하는게 어색하지 않다.

Q7. 한 Component당 핸들링하는 상태의 개수의 제한을 두는지

  • 상태가 단순히 많아진다고 문제가 되지는 않는다.
    State 하위에 성격 유형이 다른 즉, 도메인이 다른 것들이 뒤섞여 있는 것을
    한 Component에서 다루게 되면 복잡도가 높아지기 때문에 도메인별로 분리를 하는게 좋다.
    이 부분을 단순히 개수 제한으로 나누는것은 적합하지 않다.
    같은 유형인데 개수 제한에 걸려서 나눠버리면 그게 오히려 더 문제다.

Q8. TS에서 타입추론이 되는 것도 명시적으로 작성하는게 좋은지, 생략해도 되는지

  • 명시적으로 작성하는게 좋다.
    생각보다 추론되는 경우가 많다.
    컴파일러도 추론을 하지만 사람이 코드를 읽을 때도 추론이 필요하다.
    사람의 역량에 따라 보자마자 어떤 타입인지 알 수도 있지만 그렇지 않은 경우도 있기 때문에
    어떤건 추론된다고 안쓰고 어떤건 추론안된다고 쓰고 그러지말고 명시적으로 작성하는 게 좋다.
profile
다양한 사람들과 소통하며 꾸준히 성장하고 밝은 에너지를 주는 개발자

0개의 댓글