1 - Software Architecture
Frontend S/W Architecture
1.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);
}
1.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을 불러서 렌더링합니다.
1.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
2 - 시작하기
개발 환경 세팅
Fork
현재 스페이스원의 콘솔은 오픈소스로 운영중에 있습니다.
개발에 기여하기위해 먼저 console 레포지토리를 개인 github 계정에 포크해 줍니다.
Clone
이후 포크해온 레포지토리를 로컬로 클론해 줍니다.
서브모듈로 assets과 번역 관련 레포지토리가 사용중이기 때문에 함께 초기화합니다.
git clone --recurse-submodules https://github.com/[github username]/console
cd console
Run Console
콘솔을 실행 실행시키기 위해 npm으로 의존성을 설치합니다.
npm install --no-save
이후 개발환경을 위한 config 파일을 public/config/development.json
작성합니다.
(config 파일 작성은 여기를 참고 하십시오)
마지막으로 스크립트를 실행합니다.
npm run serve
Build
배포 가능한 zip을 생성하려면 아래의 스크립트를 실행하시면 됩니다.
npm run build
차트 라이선스
콘솔은 내부적으로 모든 차트에 대해 amCharts를 사용합니다.
콘솔을 사용하기 전에 amCharts의 라이선스를 확인하시고, 자신에게 맞는 amCharts 라이선스를 구매하여 콘솔에서 사용하려면 아래와 같이 진행해주십시오.
public/config/default.json
에 라이센스 키 추가
{
"AMCHARTS_LICENSE": {
"CHARTS": "",
"MAPS": "",
"TIMELINE": ""
}
}
스타일
스타일 정의에 있어 SpaceOne Console은 tailwind css와 postcss를 사용중에 있습니다.
SpaceOne의 color palette에 따라 tailwind 커스텀을 통해 적용되어 있습니다. (세부 정보는 storybook을 참고해주세요)
3 - Coding Convention
프론트엔드 코딩 컨벤션에 대한 문서입니다. Console, Design System, Core Lib
Javascript - ECMAScript 2018(ES9)
Item | Category | Rule | Example |
---|---|---|---|
Class | PascalCase | class myClass {} | |
Function | camelCase | const myFunction = () => {} | |
Variable | Readonly const Enum | SCREAMING_SNAKE_CASE | const READONLY_CONST <br /> MY_ENUM {} |
Others | camelCase | myVariable |
Typescript
Item | Category | Rule | Example |
---|---|---|---|
Type | PascalCase | type MyType = type; | |
Interface | PascalCase | interface MyInterface {} |
File / Directory / URL
Item | Repo | Category | Rule | Example |
---|---|---|---|---|
Files | Console | Vue Components | PascalCase | MyComponent.vue |
Pages | PascalCase with suffix 'Page' | MyConsolePage.vue | ||
Design System | Components Related Files | PascalCase with prefix 'P' (means 'Prime') with its component name | PMyDSComponent.vue PMyDSComponent.mdx PMyDSComponent.stories PMyDSComponent.scss PMyDSComponent.pcss | |
Type Related Files | kebab-case with suffix '-type' | type.ts schema-type.ts | ||
Other Files | Common | kebeb-case | my-config.ts | |
Directory URL | Common | kebab-case | /my-directory /my-url/ |
Code
Item | Repo | Category | Rule | Example |
---|---|---|---|---|
Event | Common | Name | Verb Root If duplicated or needed -ed / -ing is allowed | update update, updated |
Two-way Binding | If the event needs two-way biding, emit 'update:xxx' | $emit('update:code', code) | ||
Arguments | $event as the last argument | $emit(foo, bar, $event) | ||
Code | Common | Handler Name | Clear word with prefix 'handle' | const handleOnClick = () => {} |
Component Name | PascalCase at script files kebab-case at template | <my-component /> import MyComponent from @ | ||
Composition API | 1. Do not declare objects,variables inside return 2. The name of reactive variable should be state or xxxState (if needed) 3. Using variable inside setup() is recommended as reactive | 1. const a = 1; return { a } 2,3. const state = reactive({}) | ||
Console | Page script, style | 1. <script> lang should be 'ts'2. <style> lang should be 'postcss' and 'scoped' | 1. <script lang="ts"> 2. <style lang="postcss" scoped> | |
Design System | Page script, style | 1. <script> lang should be 'ts'2. <style> lang should be 'postcss' BUT NO 'scoped' | 1. <script lang="ts"> <style lang="postcss"> | |
Storybook Title | Directories + component name(PascalCase) with dash | { title: 'atoms/buttons/MyButton' ... } | ||
Root Class Name | Component name should be written on root element's class with kebab-case | <fragment class="p-my-button"> | ||
Core Lib | Description | Description of each function should be written, by JS Doc | /** @@function @name @description *@param descriptions **/ |
Additional Rules
- Array 에서 변수명 지정은 복수형보다 List 접미사를 지향합니다.
const policies: Array<string>; (X)
const plicyList: Array<string>; (O)
enum
혹은Object.freeze()
대신as const
를 사용합니다.
const NOTIFICATION_UNIT = {
PERCENT: 'PERCENT',
ACTUAL_COST: 'ACTUAL_COST',
} as const;
- API 응답 결과값에 대한 interface 명은 xxxModel 이라고 명명합니다.
interface CostQuerySetModel {};
init 할 때 실행할 함수는 setup 함수 최하단에 위치시키는 것을 지향합니다.
async 인 경우에는 즉시실행함수로 작성합니다.
(async () => {
await listCostQuerySet();
})();
return { ...toRefs(), ... }
Test Code
console과 Design System 에서는 공통적으로 vue test utils 를 사용합니다.
파일명: __test__/<대상 파일 명>.test.ts
테스트코드 템플릿
import { mount, createLocalVue } from '@vue/test-utils';
import CompositionApi, { defineComponent } from '@vue/composition-api';
const localVue = createLocalVue();
localVue.use(CompositionApi);
describe('', () => {
const mockComponent = defineComponent({
template: `
<div>
</div>
`,
setup() {
return {};
},
});
const wrapper = mount(mockComponent, { localVue });
it('', () => {
expect(wrapper.exists()).toBe(true);
});
});
Lint
References
4 - Commit Convention
프론트엔드 커밋 컨벤션에 대한 문서입니다. Console, Design System, Core Lib
커밋 메시지 구조
<타입>[적용 범위(선택 사항)]: <설명>
[본문(선택 사항)]
[꼬리말(선택 사항)]
커밋 메시지 구조적 요소
Type | Description | Remark |
---|---|---|
fix | Bug Fix API 변경 사항 없이 내부 수정 | PATCH |
feat | 기능 추가 API 변경 (하위 호환) | MINOR |
perf | 성능 향상을 위한 코드 변경 | MAJOR |
BREAKING CHANGE | API 의 변경, 큰 변화 | MAJOR |
refactor | 내부적인 리팩토링 | 앵귤러 컨벤션 |
ci | CI 변경 (workflow, etc) | 앵귤러 컨벤션 |
build | build 관련 변경 (webpack, dependencies, etc) | 앵귤러 컨벤션 |
docs | 문서 작성, 수정 | 앵귤러 컨벤션 |
style | 코드 의미적으로는 변하지 않는 커밋 (css, formatting, missing semi-colons, etc) | 앵귤러 컨벤션 |
revert | 이전 커밋으로 revert | 앵귤러 컨벤션 |
chore | 그 외 자잘한 수정 | 앵귤러 컨벤션 |
주의 사항
- 반드시 커밋 메시지 구조에 맞는 메시지를 작성하여야 합니다.
- 컨벤션과 다를 시, commitLint 에 의해 commit 이 fail 할 수 있습니다.
- 타입 뿐 아니라 적용 범위, 설명, 본문, 꼬리말 모두 영어로 작성하는 것을 지향합니다.