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

Publication

12-09 15:07

Category

'http.service'에 해당되는 글 2

  1. 2021.09.30 [MS-9] Login 화면 개발
  2. 2021.08.28 [React HH-3] 라이브러리 설정 - Axios, RxJS
2021. 9. 30. 19:48 React/Architecture

React로 Login 화면을 개발한다. 

 

 

Gateway의 api server와 web dev server의 연결

Gateway를 개발환경에서 api server를 구동하고, web 화면 테스트를 위하여 web dev server가 구동하면 web dev server의 요청이 api server로 proxy 되어야 한다. 

  • apps/gateway/web/proxy.conf.json 파일을 생성한다.
  • apps/gateway/web/project.json에 "proxyConfig" 위치와 "port"는 7000 번으로 설정한다.
// project.json 일부분 

"serve": {
      "executor": "@nrwl/web:dev-server",
      "options": {
        "buildTarget": "gateway-web:build",
        "hmr": true,
        "proxyConfig": "apps/gateway/web/proxy.conf.json", <== 요기
        "port": 7000  <== 요기
      },
      ...
}

proxy.conf.json 내역

{
  "/gateway/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/api/gateway/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/api/auth/login": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/dashboard/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/dashboard/api/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/configuration/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/configuration/api/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/back-office/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/back-office/api/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  }
}

 

 

Web 패키지들 설치

rnn-stack의 글을 참조한다.

  • antd components 패키지 설치: "yarn add antd @ant-design/icons"
  • React Router 설치: "yarn add react-router react-router-dom"
  • Axios 설치: "yarn add axios"

VSCode의 디버깅 실행 환경파일인 launch.json파일에 Web Dev Server를 수행할 수 있도록 설정한다. VSCode에서 실행보다는 별도 terminal 에서 수행하는 것을 권장하고, 옵션으로 사용한다.

// launch.json 일부 내역 
    {
      "type": "node",
      "request": "launch",
      "name": "Gateway API",
      "program": "${workspaceFolder}/apps/gateway/api/src/main.ts",
      "preLaunchTask": "build:gateway-api",
      "outFiles": ["${workspaceFolder}/dist/apps/gateway/api/**/*.js"]
    },
    // 하기 내역
    {
      "type": "node",
      "request": "launch",
      "name": "Gatewy Web",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run", "start:gateway-web"],
      "outFiles": ["${workspaceFolder}/dist/apps/gateway/web/**/*.js"]
    },

npm의 script로 "start:gateway-web"을 설정한다.

// package.json 일부 

  "scripts": {
    "build:gateway-api": "nx build gateway-api",
    "start:gateway-api": "nx serve gateway-api",
    "build:gateway-web": "nx build gateway-web",
    "start:gateway-web": "nx serve gateway-web",
    ....
  },

이제 VS Code에서 Gateway web을 실행하고, http://localhost:7000/ 호출한다.

Gateway Web Dev Server 기동

 

 

Login 화면 저작

nx 명령을 이용해 login 컴포넌트를 생성한다.

$> nx g @nrwl/react:component login --project=gateway-web

생성된 파일들

login.tsx 내역

// login.tsx
import { Row, Col, Form, Input, Button } from 'antd';
import { httpService } from '@rnm/ui';
import { LoginDto } from '@rnm/model';
import styles from './login.module.scss';

export function Login() {
  const onFinish = (user: LoginDto) => {
    httpService.post<LoginDto>('/api/auth/login', user).subscribe((result: LoginDto) => {
      console.log('Success:', result);
      // redirect dashboard
      location.href = '/dashboard';
    });
  };

  const onFinishFailed = (errorInfo: any) => {
    console.log('Failed:', errorInfo);
  };

  return (
    <div className={styles.login_container}>
      <div className={styles.center_bg}>
        <Row justify="center" align="middle" className={styles.form_container}>
          <Col span={16} offset={6}>
            <Form
              name="basic"
              layout="vertical"
              labelCol={{ span: 16 }}
              wrapperCol={{ span: 16 }}
              initialValues={{ remember: true }}
              onFinish={onFinish}
              onFinishFailed={onFinishFailed}
              autoComplete="off"
            >
              <Form.Item
                label="Username"
                name="username"
                rules={[{ required: true, message: 'Please input your username!' }]}
              >
                <Input />
              </Form.Item>

              <Form.Item
                label="Password"
                name="password"
                rules={[{ required: true, message: 'Please input your password!' }]}
              >
                <Input.Password />
              </Form.Item>

              <Form.Item wrapperCol={{ span: 16 }}>
                <Button type="primary" htmlType="submit" block>
                  Submit
                </Button>
              </Form.Item>
            </Form>
          </Col>
        </Row>
      </div>
    </div>
  );
}

export default Login;

 

 

Gateway API 서버 호출하기

Web 에서 사용할 라이브러리는 분리하고, API와 Web이 공용하는 부분은 Model에만 국한한다. 따라서 @rnm/model 패키지를 생성한다. 

$> nx g @nrwl/react:lib model --publishable --importPath=@rnm/model

domain/entities/user/user.model.ts 을 libs/model/src/lib/user/user.model.ts 로 copy하고, domain에서는 삭제한다. 그리고 user.model사용하는 클래스를 일괄 수정한다. 

// user.model.ts
export interface User {
  id?: number;
  username: string;
  password?: string;
  email?: string;
  firstName?: string;
  lastName?: string;
  role?: string;
  sub?: string | number;
  currentHashedRefreshToken?: string;
}
export type LoginDto = Pick<User, 'username' | 'password'>;
export type TokenPayload = Omit<User, 'password'>;

export enum UserRole {
  ADMIN = 'ADMIN',
  MANAGER = 'MANAGER',
  CUSTOMER = 'CUSTOMER',
  GUEST = 'GUEST',
}

 

다음으로 libs/ui/src/lib/ajax/http.service.ts 파일을 생성하고, http.service.ts 코드에, error처리 notification을 추가한다.

// http.service.ts 일부
  import { notification } from 'antd';
  
  ...
  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.abort(true);
          if (axios.isAxiosError(error)) {
            if (error.response) {
              // NestJS의 global-exception.filter.ts의 포멧
              const data: any = error.response?.data || {};
              this.showNotification(`[${data.statusCode}] ${data.error}`, data.message);
              console.log(`[${data.statusCode}] ${data.error}: ${data.message}`);
            }
          } else {
            this.showNotification('Unknow Error', error.message);
            console.log(error.message);
          }
        })
        .finally(() => {
          this.completed = true;
          observer.complete();
        });
      return () => this.abort();
    });
  }

  private showNotification(message: string, description: string): void {
    notification.error({
      message,
      description
    });
  }
}

에러처리는 libs/shared/src/lib/filter/global-exception.filter.ts 의 에러 포멧을 따른다. 

import { Request, Response } from 'express';
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const message = (exception as any).message;

    Logger.error(message, (exception as any).stack, `${request.method} ${request.url}`);

    const name = exception?.constructor?.name || 'HttpException';
    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    switch (name) {
      case 'HttpException':
        status = (exception as HttpException).getStatus();
        break;
      case 'UnauthorizedException':
        status = HttpStatus.UNAUTHORIZED;
        break;
      case 'ForbiddenException':
        status = HttpStatus.FORBIDDEN;
        break;
      case 'QueryFailedError':  // this is a TypeOrm error
        status = HttpStatus.UNPROCESSABLE_ENTITY;
        break;
      case 'EntityNotFoundError':  // this is another TypeOrm error
        status = HttpStatus.UNPROCESSABLE_ENTITY;
        break;
      case 'CannotCreateEntityIdMapError': // and another
        status = HttpStatus.UNPROCESSABLE_ENTITY;
        break;
      default:
        status = HttpStatus.INTERNAL_SERVER_ERROR;
    }

	// 에러 리턴 포멧
    response.status(status).json(
      {
        statusCode: status,
        error: name,
        message,
        method: request.method,
        path: request.url,
        timestamp: new Date().toISOString()
      }
    );
  }
}

 

테스트 진행시 UI가 Nest쪽 패키지를 사용하면 번들링 오류가 발생할 수 있다. 따라서 libs 하위의 패키지들은 향후 API용, WEB용 구분하여 사용하고, model 패키지만 공용으로 사용한다. API용, WEB용을 구분한다면 하기와 같이 별도 폴더로 묶어 관리하는게 좋아 보인다. 

api, web, model 분리

 

Nx 기반 library 생성 명령은 다음과 같다. 

// api library
$>  nx g @nrwl/nest:lib api/shared --publishable --importPath=@rnm/api-shared
$>  nx g @nrwl/nest:lib api/domain --publishable --importPath=@rnm/api-domain

// web library
$> nx g @nrwl/react:lib web/shared --publishable --importPath=@gv/web-shared
$> nx g @nrwl/react:lib web/domain --publishable --importPath=@gv/web-domain
$> nx g @nrwl/react:lib web/ui --publishable --importPath=@gv/web-ui

// model library
$> nx g @nrwl/nest:lib model --publishable --importPath=@gv/model

라이브러 생성 폴더 구조

 

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-9

 

Release ms-9 · ysyun/rnm-stack

[ms-9] added login component and enhanced ajax error notification

github.com

 

 

<참조>

- React 라이브러리 환경 구성

https://mobicon.tistory.com/580

 

[React HH-3] 라이브러리 설정 - Axios, RxJS

React 외에 애플리케이션 개발을 위한 라이브러리를 설치한다. UI Components PrimeReact, EUI, MaterialUI, AntD 검토후 소스레벨 최신으로 반영하고 있고, 다양한 비즈니스 UX 대응 가능한 AntD를 선택한다. //..

mobicon.tistory.com

- location.href와 location.replace 차이점

https://opentutorials.org/module/2919/22904

 

location.href 와 location.replace 차이점 - JavaScript Tips

[출처] [자바스크립트] location.href 와 location.replace 의 차이점.|작성자 왕따짱 location.href location.replace   기능 새로운 페이지로 이동된다. 기존페이지를 새로운 페이지로 변경시킨다.   형태 속

opentutorials.org

 

posted by peter yun 윤영식
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 peter yun 윤영식
prev 1 next