블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2021. 9. 7. 17:49 React/Start React

 props를 통해 하위 컴포넌트에 전파하는 것이 아니라 글로벌하게 접근할 수 있는 상태 저장소 사용을 위해 recoil을 사용한다. recoil 사용이유는 간결하며 Hook스타일 개발에 적합해 보인다.

 

Concept & API

1) Flexible shared state: 유연하게 상태를 공유해 보자

2) Derived data and queries: 파생으로 데이터를 만들고 조회할 수 있다.

3) App-wide state observation: 애플리케이션 전체에 걸쳐 변경을 관찰할 수 있다.

 

atom: 상태의 단위이다. 

useRecoilState: useState와 같이 read/write 가능

useSetRecoilState/useResetRecoilState: write-only

useRecoilValue: read-only, useRecoilValue(atom | selector)

selector: 파생상태를 갖거나, Async호출을 하고 싶을 경우

사용예) recoil-paint 소스

 

 

설치 및 설정

recoil을 설치한다. 

$> npm install recoil

ESLint에 recoil hook 을 설정한다. 루트의 .eslintrc.json 파일에 useRecoilCallback 을 설정한다. 

{
  "root": true,
  "ignorePatterns": ["**/*"],
  "plugins": ["@nrwl/nx"],
  "overrides": [
    {
      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
      "rules": {
        "@nrwl/nx/enforce-module-boundaries": [
          "error",
          {
            "enforceBuildableLibDependency": true,
            "allow": [],
            "depConstraints": [
              {
                "sourceTag": "*",
                "onlyDependOnLibsWithTags": ["*"]
              }
            ]
          }
        ],
        // 해당 하위 설정
        "react-hooks/exhaustive-deps": [
          "warn",
          {
            "additionalHooks": "useRecoilCallback"
          }
        ]
      }
    },
    {
      "files": ["*.ts", "*.tsx"],
      "extends": ["plugin:@nrwl/nx/typescript"],
      "rules": {}
    },
    {
      "files": ["*.js", "*.jsx"],
      "extends": ["plugin:@nrwl/nx/javascript"],
      "rules": {}
    }
  ]
}

apps/tube-csr/src/main.tsx 에 RecoilRoot를 설정한다. 

import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom';

import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import { RecoilRoot } from 'recoil';

import App from './app/app';

const queryClient = new QueryClient();

ReactDOM.render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <RecoilRoot>
        <App />
      </RecoilRoot>
      <ReactQueryDevtools />
    </QueryClientProvider>
  </StrictMode>,
  document.getElementById('root')
);

 

state 패키지 구성 및 async 적용하기

libs/state 패키지를 생성한다. 

$> nx g @nrwl/react:lib state

UPDATE workspace.json
CREATE libs/state/project.json
CREATE libs/state/.eslintrc.json
CREATE libs/state/.babelrc
CREATE libs/state/README.md
CREATE libs/state/src/index.ts
CREATE libs/state/tsconfig.json
CREATE libs/state/tsconfig.lib.json
UPDATE tsconfig.base.json
CREATE libs/state/jest.config.js
CREATE libs/state/tsconfig.spec.json
CREATE libs/state/src/lib/state.module.scss
CREATE libs/state/src/lib/state.spec.tsx
CREATE libs/state/src/lib/state.tsx

 

core, state, api의 사용관계도. 원칙으로 상호 참조는 할 수 없고, 단방향 사용만 가능하다.

core와 api 패키지는 최하단에 위치하고, app(application)은 core,state, api모두를 이용한다. state는 core, api를 이용한다.

조건)

  - state는 selector를 통해 state 변경이 요구될 때만 aync ajax 호출이 있는 경우 api 패키지를 접근한다. 

  - api는 기본적으로 모든 ajax call을 처리한다.

사용 관계도

 

State Custom Hook 을 만들어 기존의 useDogApi (기존 useDogList를 Api postfix로 명칭 변경)를 호출토록 한다. libs/state/src/lib 폴더에 dog.state.tsx를 만들고 다음과 같이 리팩토링한다. 

  • api 패키지의 useDogApi()를 호출한다. 
  • atom을 생성한다: dog 목록을 저장
  • useEffect안에서 render이후 설정한다. useEffect를 사용하지 않으면 에러가 발생한다.

without useEffect

import { useEffect } from 'react';
import { atom, useRecoilState } from 'recoil';

import { useDogApi } from '@rnn-stack/api';

export const dogAtom = atom<string[]>({
  key: 'dogAtom',
  default: [],
});

export function useDogState(): [isLoading: boolean, dog: string[], error: Error | null] {
  const { isLoading, data, error } = useDogApi();
  const [dog, setDog] = useRecoilState(dogAtom);

  useEffect(() => {
    setDog(data?.message || []);
  }, [data?.message, setDog]);

  return [isLoading, dog, error];
}

react-query의 반환값을 같이 넘긴다. useDogState Hook을 apps/tube-csr/src/app/dog.tsx에 적용한다. 즉, 기존 useDogApi 코드를 리팩토링한다. 

import { List } from 'antd';
import { useDogState } from '@rnn-stack/state';

function DogList() {
  // recoil을 사용한 custom hook 적용
  const [isLoading, dog, error] = useDogState();

  if (isLoading || !dog) {
    return <span>loading...</span>;
  }

  if (error) {
    return <span>Error: {error.message}</span>;
  }

  return (
    <List
      header={<div>Header</div>}
      footer={<div>Footer</div>}
      bordered
      dataSource={dog}
      renderItem={(item: string) => <List.Item>{item}</List.Item>}
    />
  );
}

export default DogList;

위의 경우는 api호출후 서버 state에 결과를 저장하는 과정으로 react-query를 사용해도 되지만 예로 작성해 보았다. UI에 대한 state 저장은 소개 영상을 참조한다. 

https://www.youtube.com/watch?v=_ISAA_Jt9kI&t=1s 

 

 

소스: https://github.com/ysyun/rnn-stack/releases/tag/hh-5

 

 

 

<참조> 

> 한글: https://recoiljs.org/ko/

 

Recoil

A state management library for React.

recoiljs.org

> Awesome Recoil: https://github.com/nikhil-malviya/awesome-recoil

 

GitHub - nikhil-malviya/awesome-recoil: A curated list of Recoil libraries, code snippets, best practices, benchmarks and resour

A curated list of Recoil libraries, code snippets, best practices, benchmarks and resources. - GitHub - nikhil-malviya/awesome-recoil: A curated list of Recoil libraries, code snippets, best practi...

github.com

> 2021 libraries & React state mangement

https://dev.to/sfeircode/my-go-to-react-libraries-for-2021-4k1

 

My go-to React libraries for 2021

I’ve been working on React apps for more than 3 years now and I’ve used tons of libraries to build va...

dev.to

https://dev.to/workshub/state-management-battle-in-react-2021-hooks-redux-and-recoil-2am0

 

State Management Battle in React 2021: Hooks, Redux, and Recoil

Introduction: Over the years, the massive growth of React.JS has given birth to different...

dev.to

> State Management 분류

   Flux: Redux, zustand

   Atomic: Jotai, recoil

   Proxy: mobx, valtio, overmind

https://velog.io/@gtah2yk/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%83%81%ED%83%9C%EC%97%90-%EA%B4%80%ED%95%9C-%EC%A0%95%EB%A6%AC-%EA%B8%80

 

프론트엔드 상태에 관한 정리 글

프론트엔드 개발에서 짜증나는건 상태관리 부분이다.몇 가지 좋은 글이 많아서 요약겸 정리를 하면 좋을 것 같다 생각이 든다.https://leerob.io/blog/react-state-management리액트 상태 관리의 역사를 다루

velog.io

 

> Zustand vs Redux vs Jotai vs Recoil
소스: https://github.com/redhwannacef/youtube-tutorials/blob/main/react-state-management/README.md 

https://www.youtube.com/watch?v=BaEWIbxZKco 

posted by 윤영식
2021. 9. 3. 15:22 React/Start React

react-query를 사용해 본다. 기능

  • 캐싱 기능: fresh, fetching, stale, inactive, delete 상태
    • stale 상태가 받은 데이터를 캐싱한 상태이다
  • QueryClientProvider 설정을 통해 useQuery 훅에 QueryClient를 전달한다.
  • 조회
    • const { data, isLoading, status, error, isFetching } = useQuery(queryKey, queryFunction, options) 호출
    • queryKey는 문자열, 배열이 가능하고 캐싱하는 키로 사용한다.
    • queryFuction은 promise를 반환한다.
    • isLoading: 캐시데이터 없음. 데이터 요청중 true
    • isFetching: 캐시데이터 유무와 상관 없음. 데이터 요청중 true
  • 생성/수정/삭제
    • const { mutate } = useMutation(queryFunction, options);
    • mutate(data, callbacks);
  • 캐싱 무효화
    • const queryClient = useQueryClient();
    • queryClient.invalidateQuries(key);

 

 

설치 및 설정

react-query를 설치한다. 

$> npm install react-query

apps/tube-csr/src/main.tsx에 react-query의 provider를 설정한다. 그리고 ReactQuery의 DevTool도 설치한다. 

// main.tsx
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom';

import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';

import App from './app/app';

const queryClient = new QueryClient();

ReactDOM.render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools />
    </QueryClientProvider>
  </StrictMode>,
  document.getElementById('root')
);

 ReactQuery DevTool

 

Custom Hook 만들기 및 api 패키지 구성

useQuery를 통해 custom Hook을 만들 수 있다. useQuery 사용시 Typescript에 대한 타입정의를 하자. api 패키지를 별도로 생성하고, dog.api.ts 파일을 생성한다. api custom Hook과 type 파일을 별도로 모아 둔다

$> nx g @nrwl/react:lib api

libs/api/src/lib 폴더 밑에 dog.api.ts 파일을 생성하고, react-query 의 custom Hook을 만든다. 

// dog.api.ts

import { useQuery } from 'react-query';

import { httpService } from '@rnn-stack/core';

export function useDogList() {
  return useQuery<{ message: string[]; status: string }, Error>('dog-list', () =>
    httpService
      .get('https://dog.ceo/api/breeds/list')
      .toPromise()
      .then((response: any) => response)
  );
}

dog.api.ts 파일을 libs/api/src/index.ts 에 export 추가한다. 

export * from './lib/api';

export * from './lib/api/dog.api';

 

이제 apps/tube-csr/src/app/dog.tsx 에서 사용해 본다. 기존 코드는 주석 처리하고 useDogList 라는 custom Hook을 사용한다. 

useState, useEffect 사용하지 않고 custom Hook을 별도로 작성하였다.

  • 코드가 간결해 졌다.
  • 역할 분리가 잘 되었다.
  • api는 여러곳에서 공통으로 사용하므로 위치투명하게 api 패키지로 이동하였다.
    • import { useDogList } from "@rnn-stack/api";
// dog.tsx
import { List } from 'antd';
import { useDogList } from '@rnn-stack/api';

function DogList() {
  // const [dogList, setDogList] = useState<string[]>([]);
  // useEffect(() => {
    // httpService.get('https://dog.ceo/api/breeds/list').subscribe((response: any) => {
    //   console.log('axios observable response:', response);
    //   setDogList(response.message);
    // });
  // }, []);

  const { isLoading, data, error } = useDogList();

  if (isLoading || !data) {
    return <span>loading...</span>;
  }

  if (error) {
    return <span>Error: {error.message}</span>;
  }

  return (
    <List
      header={<div>Header</div>}
      footer={<div>Footer</div>}
      bordered
      dataSource={data.message}
      renderItem={(item: string) => <List.Item>{item}</List.Item>}
    />
  );
}

export default DogList;

 

현재는 각 라이브러리의 쓰임세와 구조에 대해서 보고 있다. Production에 사용하려면 좀 더 다음어야 할 것으로 보인다. 다음은 상태관리를 위하여 recoil을 적용해 보자.

 

소스: https://github.com/ysyun/rnn-stack/releases/tag/hh-4

 

<참조>

>ReactQuery에 대한 장점을 잘 설명준다. 

https://swoo1226.tistory.com/216

 

[React-Query] 리액트로 비동기 다루기

react에서 비동기를 다루는 방법은 다양하다. javascript 언어니까 당연히 Promise, async & await으로 처리가 가능하다. redux를 사용하고 있다면, redux saga, thunk 등 다양한 미들웨어가 제공된다. 하지만 이..

swoo1226.tistory.com

 

> react-query & typescript: https://tkdodo.eu/blog/react-query-and-type-script

 

>  react hook & rxjs: https://nils-mehlhorn.de/posts/react-hooks-rxjs

 

React Hooks vs. RxJS

Here's why React Hooks are not reactive programming and how you can use RxJS knowledge from Angular in React

nils-mehlhorn.de

 

> hooks: https://twitter.com/tylermcginnis/status/1169667360795459584

  • useState: Persist value between renders, trigger re-render
  • useRef: Persist value between renders, no re-render
  • useEffect: Side effects that run after render
  • useReducer: useState in reducer pattern
  • useMemo: Memoize value between renders
  • useCallBack: Persist ref equality between renders

 

> hook lifecycle: https://medium.com/doctolib/how-to-replace-rxjs-observables-with-react-hooks-436dc1fbd324

 

How to replace RxJS observables with React hooks

As we saw in one of our previous articles, Doctolib is trying to help our new joiners to ramp up faster on our stack. One of the hardest…

medium.com

 

posted by 윤영식
2021. 8. 28. 12:24 React/Start React

React 외에 애플리케이션 개발을 위한 라이브러리를 설치한다.

 

 

 

 

 

 

UI Components

PrimeReact, EUI, MaterialUI, AntD 검토후 소스레벨 최신으로 반영하고 있고, 다양한 비즈니스 UX 대응 가능한 AntD를 선택한다.

// UI Component
$> yarn add antd

// Icon
$> yarn add @ant-design/icons

AntD의 스타일을 apps/tube-csr/src/styles.scss에 import 하고, app.tsx에 AntD의 Button 컴포넌트를 테스트 해본다. styles.scss는 글로벌 스타일로 apps/tube-csr/project.json 환경파일에 설정되어 있다.

// styles.scss
@import 'antd/dist/antd.css';


// app.tsx
import { Button } from 'antd';

import styles from './app.module.scss';

export function App() {
  return (
    <div className={styles.app}>
      <Button type="primary">Primary Button</Button>
      <Button>Default Button</Button>
      <Button type="dashed">Dashed Button</Button>
      <br />
      <Button type="text">Text Button</Button>
      <Button type="link">Link Button</Button>
    </div>
  );
}

export default App;

테스트 서버를 수행하고 확인해 본다. 

$> nx serve tube-csr

 

 

라우터 설치

CSR의 Router를 위한 react-router, react-router-dom을 설치한다.

$> yarn add react-router react-router-dom

 

 

Ajax를 위한 라이브러리 설치

Client/Server 모두 사용할 수 있는 Axios 를 설치하고 rxjs를 통해 api 를 제어할 것이다. 

$> yarn add axios

 

테스트 프로그램 작성

// dog.tsx
import { List } from 'antd';
import Axios, { AxiosResponse } from 'axios';
import { useEffect, useState } from 'react';

function DogList() {
  const [dogList, setDogList] = useState<string[]>([]);

  useEffect(() => {
    Axios.get('https://dog.ceo/api/breeds/list').then((result: AxiosResponse) => {
      setDogList(result.data.message);
    });
  }, []);

  return (
    <List
      header={<div>Header</div>}
      footer={<div>Footer</div>}
      bordered
      dataSource={dogList}
      renderItem={(item: string) => <List.Item>{item}</List.Item>}
    />
  );
}

export default DogList;


// app.tsx 에 추가
<DogList />

axios의 처리에 대해 rxjs 라이브러리를 같이 사용해 본다. 

$> yarn add rxjs

axios와 rxjs의 Observable을 사용한 라이브러리를 구현한다. 라이브러리는 NX의 libs 폴더 밑으로 생성한다. 

$> nx g @nrwl/react:lib core

// 실행을 하면 루트의 libs/core 밑으로 package가 생성된다.

libs/core/src밑으로 ajax 폴더를 생성하고 http.service.ts 파일을 생성한다. 

  • Axios와 Observable을 조합
  • Axios cancel 적용
// libs/core/src/lib/ajax/http.service.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, {
  AxiosError,
  AxiosInstance,
  AxiosPromise,
  AxiosRequestConfig,
  AxiosResponse,
  CancelTokenSource,
} from 'axios';
import { Observable, Observer } from 'rxjs';

// sample url: https://jsonplaceholder.typicode.com/users
interface RequestArgs {
  method: HttpMethod;
  url: string;
  queryParams?: any;
  payload?: any;
}

enum HttpMethod {
  GET = 'GET',
  POST = 'POST',
  PUT = 'PUT',
  PATCH = 'PATCH',
  DELETE = 'DELETE',
}

export class HttpService {
  private httpClient!: AxiosInstance;
  private cancelTokenSource!: CancelTokenSource;
  private options!: AxiosRequestConfig;
  private completed!: boolean;

  get<T>(url: string, queryParams?: any, options: AxiosRequestConfig = {}): Observable<T> {
    this.setOptions(options);
    return this.executeRequest<T>({ method: HttpMethod.GET, url, queryParams });
  }

  post<T>(url: string, payload: any, options: AxiosRequestConfig = {}): Observable<T> {
    this.setOptions(options);
    return this.executeRequest<T>({
      method: HttpMethod.POST,
      url,
      payload,
    });
  }

  put<T>(url: string, payload: any, options: AxiosRequestConfig = {}): Observable<T> {
    this.setOptions(options);
    return this.executeRequest<T>({
      method: HttpMethod.PUT,
      url,
      payload,
    });
  }

  patch<T>(url: string, payload: any, options: AxiosRequestConfig = {}): Observable<T> {
    this.setOptions(options);
    return this.executeRequest<T>({
      method: HttpMethod.PATCH,
      url,
      payload,
    });
  }

  delete<T>(url: string, options: AxiosRequestConfig = {}): Observable<T> {
    this.setOptions(options);
    return this.executeRequest<T>({
      method: HttpMethod.DELETE,
      url,
    });
  }

  cancel(forcely = false): void {
    if (!this.completed || forcely) {
      this.cancelTokenSource.cancel(`${this.options.url} is aborted`);
    }
  }

  private setOptions(options: AxiosRequestConfig = {}): void {
    if (this.options) {
      this.options = { ...this.options, ...options };
    } else {
      this.options = options;
    }
    this.cancelTokenSource = axios.CancelToken.source();
    this.httpClient = axios.create({ ...options, cancelToken: this.cancelTokenSource.token });
    this.completed = false;
  }

  private executeRequest<T>(args: RequestArgs): Observable<T> {
    const { method, url, queryParams, payload } = args;
    let request: AxiosPromise<T>;
    switch (method) {
      case HttpMethod.GET:
        request = this.httpClient.get<T>(url, { params: queryParams });
        break;
      case HttpMethod.POST:
        request = this.httpClient.post<T>(url, payload);
        break;
      case HttpMethod.PUT:
        request = this.httpClient.put<T>(url, payload);
        break;
      case HttpMethod.PATCH:
        request = this.httpClient.patch<T>(url, payload);
        break;
      case HttpMethod.DELETE:
        request = this.httpClient.delete<T>(url);
        break;
    }

    return new Observable<T>((observer: Observer<T>) => {
      request
        .then((response: AxiosResponse) => {
          observer.next(response.data);
        })
        .catch((error: AxiosError | Error) => {
          this.cancel();
          observer.error(error);
          if (axios.isAxiosError(error)) {
            console.log(error.code);
            if (error.response) {
              console.log(error.response.data);
              console.log(error.response.status);
              console.log(error.response.headers);
            }
          } else {
            console.log(error.message);
          }
        })
        .finally(() => {
          this.completed = true;
          observer.complete();
        });
    });
  }
}

export const httpService = new HttpService();

다음으로 apps/tube-csr/src/app/dog.tsx에 테스트 코드를 입력한다. httpService를 import할 때 from 구문이 Local file system에 있다하더라도 마치 node_modules에서 import하는 것처럼 위치투명성을 보장한다. 이는 루트에 위치한 tsconfig.base.json 파일에 lib 생성할 때 자동 등록된다. 

// dog.tsx

import { useEffect, useState } from 'react';

import { List } from 'antd';
// import Axios, { AxiosResponse } from 'axios';

import { httpService } from '@rnn-stack/core';

function DogList() {
  const [dogList, setDogList] = useState<string[]>([]);

  useEffect(() => {
    httpService.get('https://dog.ceo/api/breeds/list').subscribe((response: any) => {
      console.log('axios observable response:', response);
      setDogList(response.message);
    });
    // Axios.get('https://dog.ceo/api/breeds/list').then((result: AxiosResponse) => {
    //   setDogList(result.data.message);
    // });
  }, []);

  return (
    <List
      header={<div>Header</div>}
      footer={<div>Footer</div>}
      bordered
      dataSource={dogList}
      renderItem={(item: string) => <List.Item>{item}</List.Item>}
    />
  );
}

export default DogList;

tsconfig.base.json

{
  "compileOnSave": false,
  "compilerOptions": {
    "rootDir": ".",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "es2015",
    "module": "esnext",
    "lib": ["es2017", "dom"],
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@rnn-stack/core": ["libs/core/src/index.ts"]   <== 요놈
    }
  },
  "exclude": ["node_modules", "tmp"]
}

다음 포스팅에서 계속 진행...

 

소스: https://github.com/ysyun/rnn-stack/releases/tag/hh-3 

 

Release hh-3 · ysyun/rnn-stack

hh-3 add libraries and http.service.ts

github.com

 

 

posted by 윤영식
2021. 8. 25. 15:40 React/Start React

NXAngular/CLI를 확장하여 Typescript기반의 멀티 애플리케이션 및 노드 패키지개발을 위한 환경을 제공한다. 또한 Plugin 기반으로 React, NextJS, NestJS와 같은 프레임워크와 노드환경 확장을 통해 FullStack개발을 지원한다. 

 

목적

  • Case1: SPA/CSR의 React 애플리케이션 생성 + NestJS 기반 노드서버 생성
  • Case2: SSR을 위한 NextJS기반 애플리케이션 생성
  • Case1과 2의 두가지 애플리케이션에 대해 비교 테스트 진행하여 성능 이점과 차이점을 비교한다.

 

로컬에 새로운 환경 구성하기

NodeJS기반 테스트 환경 구축시 NodeJS버전을 변경하며 사용할 수 있도록 Local PC에 nvm (Node Version Manager)를 설치한다. Windows는 관련 링크를 참조하여 설치한다.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash

NodeJS LTS버전을 nvm을 통해 설치하고 사용 설정한다. 

$> nvm install v14.18.0
$> nvm use 14.18.0

소스 폴더로 이동을 하면 지정한 node version으로 nvm을 통해 switching 하고 싶다면 폴더에 .nvmrc 파일을 생성하고 "14.18.0" 버전을 명시하고, 로그인 사용자 .zshrc 또는 .bashrc 에 하기 사항을 설정한다. Gist 소스 참조

#! /usr/bin/env zsh

# Ref: https://github.com/creationix/nvm#calling-nvm-use-automatically-in-a-directory-with-a-nvmrc-file
# place this after nvm initialization!

autoload -Uz add-zsh-hook

# Function: load-nvmrc
load-nvmrc() {
    local _CUR_NODE_VER="$(nvm version)"
    local _NVMRC_PATH="$(nvm_find_nvmrc)"

    if [[ -n "${_NVMRC_PATH}" ]]; then
        local _NVMRC_NODE_VER="$(nvm version "$(cat "${_NVMRC_PATH}")")"

        if [[ "${_NVMRC_NODE_VER}" == 'N/A' ]]; then
            local compcontext='yn:yes or no:(y n)'
            vared -cp "Install the unmet version ($(cat "${_NVMRC_PATH}")) in nvm (y/n) ?" _ANSWER
            if [[ "${_ANSWER}" =~ '^y(es)?$' ]] ; then
                nvm install
            fi
        elif [[ "${_NVMRC_NODE_VER}" != "${_CUR_NODE_VER}" ]]; then
            nvm use
        fi
    elif [[ "${_CUR_NODE_VER}" != "$(nvm version default)" ]]; then
        echo -e "Reverting to the default version in nvm"
        nvm use default
    fi
}

add-zsh-hook chpwd load-nvmrc
load-nvmrc

 

NX 개발환경 구성을 위한 글로벌 패키지를 설치한다. Typescript는 v4.3.5 이상을 사용한다.

$> npm i -g @angular/cli@latest
$> npm i -g @nrwl/cli@latest
$> npm i -g yarn@latest

 

NX 개발환경 생성하기

npx 명령으로 개발환경 생성. RNN Stack에서 RNN은 React NestJS NextJS 을 합친 것이다. React Application의 명칭은 "tube-csr" 이다.

반드시 latest 버전으로 설치한다. 

$> npx create-nx-workspace@latest

선택하기
  ✔ Workspace name (e.g., org name)     · rnn-stack
  ✔ What to create in the new workspace · react
  ✔ Application name                    · tube-csr
  ✔ Default stylesheet format           · scss
  ✔ Use Nx Cloud? (It's free and doesn't require registration.) · No
   
$> cd rnn-stack

 

"tube-csr" 애플리케이션을 위한 Node서버로 NestJS 플로그인를 설치하고, "tube-api" 이름으로 서버를 생성한다.

Nx의 NestJS 플러그인 설치
$> yarn add -D @nrwl/nest@latest

생성
$> nx generate @nrwl/nest:app tube-api

 

다음으로 NX의 NextJS 플러그인을 설치한다. NextJS Application은 "tube-ssr" 이다.

설치
$> yarn add -D @nrwl/next@latest

생성
$> nx generate @nrwl/next:app tube-ssr
또는
$> nx g @nrwl/next:app tube-ssr

✔ Which stylesheet format would you like to use? · scss

 

NextJS 애플리케이션을 설치하면 sass-node버전을 v5.0.0이 설치된다. v4.14.1로 변경 사용한다. 5.0으로 사용시 컴파일 오류 발생하여 향후 패치되면 버전 업그레이드 함.

$> npm uninstall node-sass
$> npm install -D node-sass@4.14.1

tube-ssr 애플리케이션의 개발 서버 포트는 4300 으로 변경한다. workspace.json의 "tube-ssr"의 serve options설정에 포트 정보를 수정한다.

 

 

Nx 환경파일 재구성

애플리케이션과 라이브러리를 많이 만들다 보면 rnn-stack/workspace.json 파일안에 설정이 계속 추가된다. 가독성을 위하여 애플리케이션(라이브러리 포함) Nx의 환경설정을 프로젝트별 별도 파일로 분리한다. 분리후에는 애플리케이션이나 라이브러리 생성시 자동으로 "project.json" 파일로 분리가 된다.

 

각 애플리케이션 root 폴더에 "project.json" 파일을 생성하고 workspace.json의 프로젝트별 설정 정보를 옮긴다. workspace.json에는 애플리케이션의 위치정보로 수정하면 컨벤션에 의해 project.json을 인지한다.

workspace.json은 애플리케이션 위치를 표현한다. 

 

테스트하기

"tube"라는 React 애플리케이션을 Dev Server기반으로 실행하고, "realtime"이라는 NextJS 프렘워크기반 노드 서버를 실행한다. 

  • React Application: http://localhost:4200/ 
  • NextJS Application: http://localhost:4300/
React Single Page Application
$> nx serve tube-csr

NextJS Application with Server
$> nx serve tube-ssr

 

Prettier 코드 포멧터 설정하기

MS Code 편집기를 기준으로 prettier를 설정한다. 

  • rnn-stack 루트 폴더에 .prettierrc파일을 생성한다.
  • MS Code를 위한 Prettier Plugin을 설치한다. 
  • .vscode/settings.json 에 prettier 옵션을 설정한다. settings.json 파일이 존재하지 않다면 생성후 설정한다.
  • .vscode/extensions.json의 recommandation으로 prettier를 설정한다.

.prettierrc 내역

{
    "printWidth": 120,
    "singleQuote": true,
    "useTabs": false,
    "tabWidth": 2,
    "semi": true,
    "bracketSpacing": true
}

. vscode/settings.json 내역

{
    "editor.formatOnPaste": true,
    "editor.formatOnType": true,
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[scss]": {
        "editor.suggest.insertMode": "replace",
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[html]": {
        "editor.suggest.insertMode": "replace",
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    }
}

.vscode/extensions.json 내역

{
  "recommendations": [
    "esbenp.prettier-vscode",
    "nrwl.angular-console",
    "firsttris.vscode-jest-runner",
    "dbaeumer.vscode-eslint"
  ]
}

 

소스

https://github.com/ysyun/rnn-stack/releases/tag/hh-2

 

참조

posted by 윤영식
2021. 8. 25. 10:48 React/Start React

리액트 프로젝트를 위해 조사해봐야할 항목에 대해서 정리해 본다. 하기 구성에 대해 소스레벨의 준비과정을 작성해 본다.

 

개발환경

  • micro-frontend를 지원하는 환경
  • mono-repo기반 multi application을 개발할 수 있는 환경
  • Nx.dev for React
    • 멀티 애플리케이션 개발 및 애프리케이션별 번들링
    • 라이브러리의 위치 투명성 보장 및 배포

 

구조 설계

  • 1Layer: 애플리케이션
    • Layout: Feature 를 다루는 영역을 분할
    • Feature: UI + Data-Access 를 조합하여 업무 단위 화면을 구성
  • 2Layer: 업무 라이브러리
    • UI: Layout, Feature의 부분을 담당하는 업무 컴포넌트
    • Data-Access: API 호출 및 Data Caching
    • Data-Type: Frontend - Backend간 데이터 타입 및 vadliation
    • State: UI 상태 저장 및 전파
  • 3Layer: 공용 라이브러
    • Component: 사용자 정의 요소 컴포넌트 및 랩퍼
    • Chart: 차트 및 랩퍼
    • Utils: 유틸리티들
  • 4Layer: package.json 
    • AntD, PrimeReact, MaterialUI, Boostrap와 같은 CSS + Component Framework
    • Redux, Mobx, Recoil 과 같은 state
    • moment, date-fns 같은 date
    • immer 같은 immutable state 
    • react-use 같은 Hooks
    • react-query 같은 data api
    • rxjs 같은 reactiv library

 

고려 사항

  • Elagant Code Base 유지
    • Code Convention
    • TSLint
    • Typescript
  • Unit Test Code 
    • commit 전에 Unit Test 수행
  • Loose Coupling을 위한 UI Biz Feature, UI Componnet, Library 관계 정립

 

 

to be continue...

posted by 윤영식