Software Architecture

Software Architecture

Frontend S/W Architecture

1 - Frontend Error Handling

How To Handle Error

Error 구분에 따른 처리 (API)


다양한 API Error를 처리하기 위해 미리 에러 타입을 정의한다.

  • NotFoundError : 리소스를 찾을 수 없을 때 (status: 404)

  • BadRequestError : 잘못된 요청을 보낼 때 (status: 400)

  • AuthenticationError: 인증이 실패했을 때 (status: 401)

  • AuthorizationError: 권한 검증에 실패했을 때 (status: 403)

  • APIError: 이외의 모든 API Error 및 서버 에러 (status: 500 등)

이 에러들을 axios api client의 interceptor에서 받아 핸들링한다. 위에 명시된 에러 이외에 다른 에러 코드를 정의할 수도 있다.


이 에러들을 SpaceConnector(axios api client)의 interceptor에서 받아 핸들링한다.

axios의 interceptor에서 error가 왔을 때, 각 statusCode를 보고 판별하여 위에 미리 정의해둔 Error로 throw해준다.

Error Handler (Client)


Client 쪽에서 API client를 통해 넘어온 Error의 유형을 판단하여 다음과 같이 핸들링한다.

AuthenticationError의 경우

Token이 유효한 지 확인하고, 유효하지 않을 경우 Authentication Error Handler를 호출하여 에러를 처리한다.

Authentication Error Handler는 다음과 같은 동작을 한다.

  • Session 만료 모달을 보여줌

  • User의 Session을 만료시킴

AuthorizationError의 경우

Authorization Error Handler를 호출한다. 이 핸들러는 다음과 같은 동작을 한다.

상단에 권한이 없다는 경고창을 노출시킴

NoResourceError의 경우

No Resource라는 토스트를 보여주고, redirect url이 있을 시 해당 url로 이동시킨다.

APIError의 경우

콘솔창에 API Error라고 명시된 로그를 남긴다.

BadRequestError의 경우

페이지에서 넘겨준 Error Message가 적힌 toast 경고창을 띄운다.


Error를 추가하는 방법


  1. SpaceConnector(console-core-lib > space-connector/error.ts)에 새로운 에러를 정의한다.

  2. SpaceConnector의 Error Interceptor(console-core-lib > space-connector/api.ts)에 새로운 에러를 throw해주는 부분을 추가해준다.

  3. API의 error가 아니라 Client 자체에서 필요한 에러의 경우 Client(console > common/error.ts)에 새로운 에러를 정의하고, 에러가 발생할 시 해당 에러를 throw한다.

  4. Error Handler(console > common/composables/error/errorHandler.ts)에 새롭게 추가된 Error를 핸들링하는 코드를 추가한다.

Error를 사용하는 방법


에러 클래스를 작성하고, 그 에러 클래스를 이용해 만든 에러 핸들러를 실제 프로젝트에서는 아래와 같이 사용한다.


  • list 등 일반적인 작업
try {
    특정 동작
} catch (e) {
    ErrorHandler.handleError(e);
}
  • create/update 등 실패 시 toast message를 보여주어야 하는 작업
try {
    특정 동작
} catch (e) {
    ErrorHandler.handleRequestError(e, 번역을 위한 i18n key값 or string);
}

2 - Frontend Authentication

How To Authenticate SpaceONE User

인증 플로우


SpaceONE은 현재 도메인 설정에 따라 Google Oauth2, Keycloak, KB SSO 세 가지의 인증 중 하나를 선택하여 제공하고, 인증 플로우는 다음과 같습니다.
![](/docs/developers/frontend/software_design/authentication/authentication_img/authentication_uml.png)
1. 우선 해당 도메인에서 어떤 Sign-in 방식(ID/PW인지, 외부 인증을 제공하는 지, 제공한다면 어떤 인증방식인지)을 사용하는 지 체크합니다. 2. 그 후, 각각 다른 Sign-in 방식에 맞는 UI를 보여주고, 해당 템플릿에서 Authenticator를 상속받은 커스텀 인증 모듈의 메소드를 호출합니다. 3. 각각의 커스텀 Auth들은 각자 필요한 Sign-in 절차를 수행한 후, 상속받은 Authenticator의 기본 Sign-in 로직을 수행합니다.

이후 이루어지는 인증 플로우는 다음과 같습니다.

Authenticator


Authenticator는 다음과 같은 구조를 가집니다.


최상위 Authenticator는 아주 기본적인 signIn과 signOut 메서드만 가지는 추상 클래스입니다.

abstract class Authenticator {
	static async signIn(필요한 매개변수): Promise<void> {
    	// sign in 로직
    }

    static async signOut(): Promise<void> {
    	// sign out 로직
    }
}

export {
	Authenticator,
}

signIn을 위해 백엔드 인증 서버에 보낼 credentials라는 데이터가 필수적이고, userId와 userType(일반 User인지, 관리자인지, API만 사용하는 API user인지)을 부수적으로 받습니다. signOut에서는 vuex에 작성해놓은 signOut 메서드를 호출합니다.


## 커스텀 인증 구조
이제 기본적인 추상 클래스 작성이 끝났으니, 이 클래스를 상속받아 각 SSO에 맞는 Auth 클래스들을 작성합니다. 위의 구조도를 보면 알 수 있듯이, Auth를 구현하는 데에 필요한 것은 두 가지가 있습니다. 바로 폼 렌더링(SSO 버튼 등)을 위한 템플릿과, 메서드들을 구현한 module(.ts) 파일입니다.

커스텀 인증 클래스


각 커스텀 인증에 맞는 Sign In, Sign Out 로직과 추상 클래스 Authenticator의 인증(기본 인증)을 수행하는 로직이 필요합니다. Authenticator의 Sign In은 credentials라는 data를 필요로 하고, 이 credentials 안에 들어가는 데이터는 모든 커스텀 인증마다 상이합니다.
// custom-auth.ts

class CustomAuth extends Authenticator {
    const signIn = async () => {
        // 각 SSO, 커스텀 인증에 필요한 Sign in 로직
        const credentials = { // 각 커스텀 인증에서 생성된 credentials }
        super.signIn(credentials);
    }

    const signOut = async () => {
        // 각 SSO, 커스텀 인증에 필요한 Sign Out 로직
        super.signOut();
    }
}

결론적으로, 커스텀 Auth class 내부에 상속받은 Authenticator(=super)의 함수들을 호출하여 SpaceONE의 인증을 동시에 수행할 수 있도록 합니다.

커스텀 인증 클래스 loader


이제 어떤 인증 클래스를 부를 건지 결정하는 loader를 간단하게 만들어보도록 합니다.
export const loadAuth = (authType?) => {
    if (authType === 'CUSTOM_AUTH') return CustomAuth;
    return SpaceAuth;
};

SignIn은 개별 템플릿을 가지지만, SignOut은 어디에서든 호출될 수 있기 때문에 위와 같은 loader를 사용하여 현재 도메인의 인증 타입을 넣으면 알맞은 인증 로직을 수행할 수 있도록 작성합니다.

커스텀 인증 템플릿


위의 작업을 하고난 후 마지막 작업은 폼 렌더링입니다.
//Custom Auth의 template(external/custom/template/CUSTOM_AUTH.vue)

<template>
	<div>Custom 로그인을 위한  버튼</div>
</template>

<script lang="ts">
 setup() {
 	//필요한 로직들
 	onMounted(async() => {
    		try {
            	await loadAuth('CUSTOM_AUTH').signIn(); //loader를 사용하여 customAuth 클래스의 signIn 함수 호출
            } catch (e) {
            	//에러 핸들링
            }
        }
    )
 }
</script>

각 커스텀 인증은 저마다의 폼 렌더링이 필요하기 때문에 위와 같이 해당 Auth에 맞는 커스텀 템플릿을 작성해줍니다.
그리고 마지막으로 커스텀 템플릿을 SignIn Page에서 보여줍니다. vue에는 라는 문법으로 다이나믹하게 컴포넌트를 불러올 수 있는 문법이 존재합니다.(https://kr.vuejs.org/v2/guide/components-dynamic-async.html) 해당 문법을 이용하여

//sign-in page
<component :is="component" class="sign-in-template"
	@sign-in="handleSignIn"
/>

이렇게 템플릿 부분에 작성해주고, 아래 스크립트 부분에서

//sign-in page
  const state = reactive({
  	...
	component: computed(() => {
                let component;
                const auth = state.authType;
                if (auth) {
                    try {
                        component = () => import(`./external/${auth}/template/${auth}.vue`);
                    } catch (e) {
                        //필요한 에러 핸들링.
                    }
                }
                return component;
            }),
  })

위와 같이 dynamic import 방식을 사용하여 컴포넌트를 렌더링합니다.

TL;DR

  1. 외부 인증 뿐만 아니라 자체 인증 또한 성공해야 하기 때문에 자체 인증만을 구현한 추상 클래스를 만듭니다.
  2. 해당 추상 클래스를 상속하여 구현한 커스텀 인증 클래스들을 만들어 해당 클래스 내부에서 커스텀 인증 로직을 처리하고, super class의 인증도 처리합니다.
  3. 해당 커스텀 클래스를 불러오기 위한 loader를 추가합니다.
  4. 커스텀 클래스의 폼을 렌더링하기 위한 template을 작성해줍니다.
  5. Sign In 페이지에서 다이나믹하게 해당 template을 불러서 렌더링합니다.

3 - Config Management

How To Manage Frontend Configuration

App에 필요한 구성 요소를 정의한다. 주로, 환경 별로 다른 값을 다룬다.

환경 별 우선순위

default.json이 우선적으로 적용된다.

development 환경일 경우, development.json의 값이 덮어 쓰여 적용된다.

환경 설정 방법

Dockerfile 수정

Dockerfile에서 npm run build 커맨드 전 NODE_ENV로 환경 설정 가능

...
ENV NODE_ENV development
...

Webstorm Configurations 수정

Run/Debug Configuration 설정 시 Environment 필드값 수정

Default Config 정보

NameDescription
CONSOLE_API콘솔에서 사용하는 API의 엔드포인트를 정의
GTAG_IDGoogle Analytics를 위해 사용
DOMAIN_NAME사이트 도메인 이름
DOMAIN_NAME_REF‘hostname’ 일 경우, 사이트 도메인 이름을 추출하여 Domain 정보 로드
ADMIN_DOMAIN
AMCHARTS_LICENSE차트 라이브러리인 amcharts의 라이센스 정보
MOCKMOCK API 사용 여부 및 MOCK API의 엔드포인트 정의
ASSET_PATHasset에 사용되는 엔드포인트 정보
DOMAIN_IMAGESignIn 페이지 및 GNB에 사용되는 이미지의 url 정의
DOCS관련 문서 링크를 만들기 위한 정보
- label, link 를 가진 객체 배열
- ejs template 문법을 지원
 - 제공 변수: lang (사용자 언어 코드. e.g. "en")
BILLING_ENABLEDbilling 서비스 이용 가능한 도메인 리스트 정의
CONTACT_LINKSignIn 페이지의 contact us 링크 정의
  • development.json 권장 예시
    {
        "VUE_APP_API": {
          "ENDPOINT": "https://sample.com"
        },
        "GTAG_ID": "DISABLED",
        "DOMAIN_NAME": "sample",
        "DOMAIN_NAME_REF": "config",
        "ASSETS_ENDPOINT": "https://sample-asset.com/assets/"
    }
    

Config 파일 위치

  • Default: <SOURCE_ROOT>/public/config/default.json
  • Each Environment: <SOURCE_ROOT>/public/config/<NODE_ENV>.json

Config 사용 방법

import config from '@/lib/config'

config.get(); // All Values
config.get('VUE_APP_API.ENDPOINT'); // Value of specific key