Frontend S/W Architecture
1 - Frontend Error Handling
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를 추가하는 방법
SpaceConnector(console-core-lib > space-connector/error.ts)에 새로운 에러를 정의한다.
SpaceConnector의 Error Interceptor(console-core-lib > space-connector/api.ts)에 새로운 에러를 throw해주는 부분을 추가해준다.
API의 error가 아니라 Client 자체에서 필요한 에러의 경우 Client(console > common/error.ts)에 새로운 에러를 정의하고, 에러가 발생할 시 해당 에러를 throw한다.
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
인증 플로우
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에는
//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
- 외부 인증 뿐만 아니라 자체 인증 또한 성공해야 하기 때문에 자체 인증만을 구현한 추상 클래스를 만듭니다.
- 해당 추상 클래스를 상속하여 구현한 커스텀 인증 클래스들을 만들어 해당 클래스 내부에서 커스텀 인증 로직을 처리하고, super class의 인증도 처리합니다.
- 해당 커스텀 클래스를 불러오기 위한 loader를 추가합니다.
- 커스텀 클래스의 폼을 렌더링하기 위한 template을 작성해줍니다.
- Sign In 페이지에서 다이나믹하게 해당 template을 불러서 렌더링합니다.
3 - Config Management
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 정보
Name | Description |
---|---|
CONSOLE_API | 콘솔에서 사용하는 API의 엔드포인트를 정의 |
GTAG_ID | Google Analytics를 위해 사용 |
DOMAIN_NAME | 사이트 도메인 이름 |
DOMAIN_NAME_REF | ‘hostname’ 일 경우, 사이트 도메인 이름을 추출하여 Domain 정보 로드 |
ADMIN_DOMAIN | |
AMCHARTS_LICENSE | 차트 라이브러리인 amcharts의 라이센스 정보 |
MOCK | MOCK API 사용 여부 및 MOCK API의 엔드포인트 정의 |
ASSET_PATH | asset에 사용되는 엔드포인트 정보 |
DOMAIN_IMAGE | SignIn 페이지 및 GNB에 사용되는 이미지의 url 정의 |
DOCS | 관련 문서 링크를 만들기 위한 정보 - label, link 를 가진 객체 배열 - ejs template 문법을 지원 - 제공 변수: lang (사용자 언어 코드. e.g. "en") |
BILLING_ENABLED | billing 서비스 이용 가능한 도메인 리스트 정의 |
CONTACT_LINK | SignIn 페이지의 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