클린 아키텍처 원칙에 따른 레이어 기반의 타입스크립트 프로젝트 템플릿 생성 가이드
이것이 바로 여러분들이 평생 동안 찾아다닌 그것입니다. 뭘까요? 지난날의 코드를 디버깅하는 고통, 불행 그 많은 시간들은 이제 끝입니다. 앱이 3시간 안에 완성될 것이라고 생각하며 작성했던, 혹은 노력할만한 가치가 없는 데모 앱을 만든다고 생각하며 작성했던, 또는 여러분들이 더 잘 모르며 작성했던 그 코드들이요.
이 가이드에서는 클린 아키텍처 원칙을 구현하는 타입스크립트 프로젝트 템플릿을 만드는 방법을 설명합니다. 이것은 Anglar, React, Vue 또는 React Native 또는 Electron과 같은 다른 프레임워크가 있는 SPA(싱글 페이지 어플리케이션)를 포함하여 생각할 수 있는 모든 유형의 애플리케이션에 사용할 수 있습니다. 기본적으로 타입스크립트를 사용하면 적용할 수 있습니다.
클린 아키텍처는 앱을 작성할 때, 덜 고통스럽게 하기 위해 몇 가지 규칙을 따르기만 하는 것이 아닙니다. 적절한 아키텍처 및 종속성 규칙을 적용할 수 있도록 작업 환경을 혁신하는 것입니다. 여러분들은 아마도 미래의 어느 시점에서 게으름을 피우고 가능한 빨리 기능을 개발하기 위해 규칙을 완전히 무시하는 것을 허락하지 않을 것입니다. 왜냐하면 그것은 여러분들에게 악영향으로 되돌아 올 것이기 때문입니다.
Uncle Bob's의 클린 아키텍처(한 번 꼭 읽어 보는 것을 추천드립니다.) 이면에 있는 아이디어는 관심사를 분리하는 것입니다. 이를 통해 코드가 프레임워크, 데이터베이스 또는 UI와 독립적일 수 있습니다. 또한 코드를 보다 쉽게 테스트할 수 있습니다.
이 다이어그램은 클린 아키텍처의 핵심 아이디어인 종속성 규칙을 시각화 하고 있습니다. 모든 화살표가 엔티티를 향해 안쪽을 가리킵니다. 이것은 엔티티는 무엇에도 의존하지 않지만 모든 것이 엔티티를 의존한다는 것을 알 수 있습니다. 이는 엔티티가 비지니스 규칙을 나타내며 사용자가 Angular, React, Vue 등 무엇을 사용하든 상관하지 않기 때문입니다.
즉, 엔터프라이즈 비지니스 규칙은 엔터프라이즈/애플리케이션의 핵심 규칙을 정의하는 클래스 또는 모델을 구현하는 계층입니다.
여러분들은 엔티티와 상호작용하기 위해 엔티티를 둘러 싸고 있는 유즈 케이스를 계층을 사용합니다. 이 계층은 엔티티로 들어오고 나가는 데이터 흐름을 지시하고, 아래 계층들이 엔티티들과 상호 작용할 수 있도록 합니다.
그 다음, 다이어그램의 오른쪽 하단 섹션에서 볼 수 있듯이 컨트롤러는 프레젠터에 사용되는 출력을 생성하기 위해 유즈케이스와 상호 작용합니다. Angular, React 컴포넌트들이 이곳에 위치 되며, 프레젠터와 컨트롤러 역할을 모두 맡게 됩니다.
마지막으로 가장 바깥쪽 계층에 데이터베이스와 같은 세부 정보가 있습니다. 즉, 안쪽의 원을 변경하지 않고도 가장 바깥쪽 레이어의 세부 정보를 변경할 수 있습니다. 이해가 가시나요? 웹 프레임워크는 당신이 MongoDB, SQL, 심지어 블록체인을 사용하든 상관하지 않습니다. 그리고 물론 유즈케이스나 엔티티는 그러한 도구들에서 멀리 떨어져 있습니다.
시작하기 전에 몇 가지 언급할 것이 있습니다.
먼저 애플리케이션 템플릿을 코어, 데이터, 의존성 주입(DI), 프레젠테이션과 같이 4개의 레이어로 구분했습니다.
두 번째로, 저는 여기서 Lerna를 사용하는 것을 선호했습니다. 왜냐하면 그것이 저에게 가장 적은 두통을 주었기 때문입니다. Yarn 또는 Npm Workspaces 와 같은 대안이 있지만 Lerna 만큼 쉽지 않았습니다.
마지막으로, 일부 섹션에서 제 접근 방식에 동의하지 않을 수 있지만, 이 아키텍처의 장점은 앱의 코어가 변경되지 않고 앱의 종속성이 자체에만 있다면 무엇이든 바꿀 수 있다는 것입니다.
말이 나왔으니 시작해보겠습니다.
root(루트)
라고 부르겠습니다.core
, data
, di
라는 세가지 폴더를 생성합니다.여러분들은 이제 다음과 같은 구조를 가지고 있습니다.
다음으로, 우리는 이것들을 모두 초기화해야 합니다. root
, core
, data
, di
에서 다음 명령을 수행합니다.
npm init -y
를 실행합니다.tsconfig.json
파일을 생성합니다. 아래 제공된 구성을 사용하세요.루트의 tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
}
}
core
, data
의 tsconfig.json
{
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
"baseUrl": ".",
"rootDir": ".",
"module": "ES2020",
"target": "ES2020",
"moduleResolution": "node",
"outDir": "build/",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"plugins": [
{
"name": "@rollup/plugin-typescript"
}
]
}
}
di
의 tsconfig.json
{
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
"baseUrl": ".",
"rootDir": ".",
"module": "ES2020",
"target": "ES2020",
"moduleResolution": "node",
"lib": ["ES6", "DOM"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "build/",
"plugins": [
{
"name": "@rollup/plugin-typescript"
}
]
}
}
마지막으로 프레젠테이션을 만듭니다. 여러분들은 이것을 여러분이 원하는 대로 할 수 있습니다. 저는 Angular를 선호하기 때문에 Angular CLI를 사용하여 생성합니다. Angular CLI를 사용하여 프로젝트를 초기화하려면 패키지 폴더에서 ng new
명령어를 실행하고 취향에 따라 단계를 수행합니다.
그리고 루트와 모든 패키지에 .gitignore
파일을 추가하였습니다. /node_modules
와 /build
를 추가하여 node_modules
와 빌드된 내용을 무시하도록 설정하였습니다.
여기까지 진행하면 다음과 같은 모습이 됩니다.
다음으로 core
, data
, di
를 설정할 것입니다. 모두 동일한 초기 설정을 따릅니다. 따라서 모든 항목에 대해 다음 단계를 따릅니다. (또는 코어에서 이 작업을 수행한 다음 모든 항목을 복사하여 데이터 및 di에 붙여넣습니다.)
먼저 src
폴더를 생성하고 index.ts
라는 파일을 추가합니다. 이 파일은 우리가 우리의 패키지에서 내보내고자 하는 모든 것을 표현합니다. src
내부에 tests
라는 폴더를 만듭니다. 여기에는 이 패키지에 대한 유닛 테스트가 포함됩니다.
package.json
에 추가해야할 몇 가지 스크립트와 devDependencies
가 있습니다. 다음을 복사하여 붙여넣을 수 있습니다. (데이터 및 di 페키지에서는 이름을 변경해 주세요)
{
"name": "core",
"version": "1.0.0",
"description": "",
"scripts": {
"build": "tsc --project tsconfig.build.json && npm run rollup",
"build:watch": "nodemon -e ts --exec \"npm run build\" --ignore build/** --ignore tests/**",
"rollup": "rollup -c",
"test": "jest",
"test:watch": "jest --watch"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-typescript": "^8.3.3",
"@types/jest": "^28.1.6",
"@types/node": "^18.0.0",
"jest": "^28.1.3",
"nodemon": "^2.0.19",
"rollup": "^2.77.0",
"rollup-plugin-dts": "^4.2.2",
"ts-jest": "^28.0.7",
"typescript": "^4.7.4",
"tslib": "^2.4.0"
},
"main": "build/src/index.js",
"types": "build/src/index.d.ts"
}
파일 마지막에 있는 types
속성을 한번 알아봅시다. 이 패키지를 다른 패키지에서 가져오고 사용할 수 있도록 하려면 이 속성이 필요합니다.
각 스크립트에 대한 설명은 다음과 같습니다.
build
: 프로젝트를 빌드하면 이 패키지를 다른 패키지에서 가져올 수 있는 index.d.ts
가 포함된 빌드 폴더가 생성됩니다.build:watch
: 코드가 변경될 때마다 프로젝트가 재빌드 됩니다.rollup
: 빌드 후 프로젝트 타입을 번들합니다. 올바른 선언 및 내보내기를 포함하는 index.d.ts
를 생성하려면 이 작업이 필요합니다.test
: 패키지의 모든 단위 테스트를 실행합니다.test:watch
: 모든 관련 변경 사항에 대해 테스트를 다시 시작합니다. (자세한 내용은 jest 문서 참조)devDependencies
에 대한 설명은 다음과 같습니다.
rollup
, rollup-plugin-dts
, @rollup/plugin-typescript
: index.d.ts
파일을 유즈케이스에 더 유용하도록 수정하는 데 사용되는 모듈 번들러입니다.jest
, ts-jest
: 테스트 작성 및 단위 테스트용nodemon
: 코드가 변경될 때마다 명령을 실행할 수 있습니다. build:watch
스크립트를 참조하세요.rollup과 jest를 위해 몇 가지 설정 파일을 추가해야 합니다. 이 설정 파일들은 package.json
과 동일한 수준입니다. 다음과 같습니다.
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import tsConfig from "./tsconfig.json";
const config = [
{
input: "src/index.ts",
output: [{ file: "build/src/index.js", sourcemap: true }],
plugins: [
typescript({
sourceMap: tsConfig.compilerOptions.sourceMap,
}),
],
},
{
input: "build/src/index.d.ts",
output: [{ file: "build/src/index.d.ts", format: "es" }],
plugins: [
dts({
compilerOptions: {
baseUrl: tsConfig.compilerOptions.baseUrl,
},
}),
],
},
];
export default config;
module.exports = {
transform: { "^.+\\.ts?$": "ts-jest" },
testEnvironment: "node",
testRegex: ".*\\.(test)?\\.(ts)$",
moduleFileExtensions: ["ts", "js"],
};
마지막으로 한 가지: 빌드에 테스트가 포함되어 있지 않길 바라므로 tsconfig.json
옆에 tsconfig.build.json
라는 파일을 새로 만듭니다. 이 파일은 package.json
안의 빌드 스크립트를 사용합니다. 내용은 다음과 같습니다.
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "src/tests"]
}
다 됐습니다!
이제 각 패키지의 모양은 다음과 같습니다.
이제 패키지를 테스트 빌드 할 수 있습니다. 빌드에서 어떤 작업을 수행하려면 패키지의 index.ts
파일에 내보내기를 추가하면 됩니다. 예: export const Foo = 1;
이제 npm install && npm run build
명령어를 각 코어, 데이터, di에서 실행합니다.
정상적으로 빌드가 진행 되었다면, build/src/index.ts
, build/src/index.d.ts
파일에서 빌드 결과를 확인할 수 있습니다.
이제 프로젝트 패키지들은 준비 되었습니다.
현재로서는 서로 관련 없는 5개의 패키지(root
, core
, data
, di
, presentation
)만 있습니다. 우리는 다른 패키지의 컨테이너 역할을 하기 위해 root
가 필요합니다. 그러기 위해 저희는 Lerna를 사용할 것입니다.
먼저 다음 설정을 복사하여 루트의 package.json
에 붙여넣습니다.
{
"name": "root",
"license": "ISC",
"scripts": {
"prestart": "npm install && lerna bootstrap",
"start": "lerna run start",
"test": "lerna run test",
"build": "lerna run build",
"graph": "nx graph"
},
"devDependencies": {
"lerna": "^5.1.8",
"nx": "^14.4.3",
"tslib": "^2.4.0"
}
}
다음은 스크립트에 대한 설명입니다.
prestart
: 이 스크립트는 npm start
를 실행하면 자동으로 실행됩니다. 모든 패키지에 대한 모든 종속성이 있고 올바르게 구성되었는지 확인합니다.start
: packages
디렉토리의 모든 패키지에 대해 시작 스크립트를 실행합니다.test
, build
: start
와 동일합니다.graph
: 프로젝트 종속성을 시각화할 수 있도록 종속성 그래프를 만듭니다.Lerna 및 NX에 대한 종속성은 스크립트에서 사용하므로 필요합니다.
다음으로 Lerna와 NX의 설정 파일(각각 lerna.json
및 nx.json
)이 있습니다.
lerna.json
{
"packages": ["packages/*"],
"useNx": true,
"version": "0.0.0"
}
nx.json
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test"]
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"]
},
"start": {
"dependsOn": ["^build"]
}
}
}
nx.json
에서 cacheableOperations
라는 속성을 볼 수 있습니다. 이 속성은 변경된 패키지만 리빌드/리테스트됩니다. 프로젝트가 커질수록 이 기능에 더욱 감사하게 될 것입니다.
또한 targetDefaults
에는 start
항목과 build
항목, 이 두 가지 항목이 포함되어 있습니다. 여기서의 설정은 패키지 A가 패키지 B에 종속된 경우 패키지 A의 시작은 패키지 B의 빌드 후에만 실행되도록 합니다. 지금 당장 명확하지 않다면 다음 섹션에서 이 작업이 왜 필요한지 알 수 있습니다.
마지막 단계만 진행하면 이제 끝입니다. 우리는 data
, presentation
, di
가 core
에 의존한다고 아직 명시하지 않았습니다. 다음 단계는 이것을 할 것입니다.
data/package.json
안에 devDependencies
에 dependencies: { "core": "*" }
속성을 추가합니다.
di/package.json
안에 devDependencies
에 dependencies: { "core": "*", "data": "*" }
속성을 추가합니다.
presentation/package.json
안에 devDependencies
에 dependencies: { "core": "*", "di": "*" }
속성을 추가합니다.
Nx와 Lerna는 presentation
이 다른 패키지들에 의존하고 있으므로 presentation
을 시작하기 전에 core
, data
, di
를 빌드할 수 있을 만큼 똑똑합니다.
공식적으로 끝이 났습니다! 이제 루트에서 npm start
를 실행하고 어떤 마법 같은 일이 일어나는지 지켜보세요.
만약 오류가 발생한다면 모든 폴더에서 package-lock.json, node_modules를 제거하고 다시 npm start
를 실행하세요.
이 템플릿을 사용하는 방법을 단계별로 설명하는 제 글이 있습니다. 링크는 여기에서 찾을 수 있습니다.
저는 한동안 클린 아키텍처로 프로젝트를 구성하기 위해 좋은 방법을 찾으려고 노력했습니다. 저는 모든 것을 하나의 패키지에 넣고 규칙을 준수할 수 있다고 믿는 가장 빠른 접근 방식을 고려했습니다. 왜냐하면 저는 훌륭한 프로그래머이기 때문입니다. (그렇게 믿고 싶었습니다.) 하지만 이 방법은 저에게 충분하지 않았습니다.
저는 규칙을 어기지 않을 무언가가 필요했습니다. 여기서 말하는 규칙은 의존성 규칙입니다. 엔티티가 데이터베이스 혹은 UI 코드에 종속되도록 허용할 수 없습니다. 잘못된 관계에 의존성이 생기면 오류를 발생시키는 스크립트를 포함 하는( dependency-cruiser 사용) 방법을 통해 해결하려고 했지만 까다로웠습니다. 그리고 너무 늦게 오류를 보여주었습니다.
그래서 각 애플리케이션 계층 자체로 패키지를 나누기로 했습니다. 자체 package.json
, tsconfig.json
및 설정된 번들로 완성하였습니다. 그리고 Lerna와 Nx를 통해 각 패키지를 통합하였습니다. 이러한 방식으로는 package.json
에 의존성을 명시하지 않는 한 각 패키지들은 "우연히" 혹은 어떤 방법으로든 의존할 수 없습니다. 제 입으로 말하자면 이것은 예술적으로 동작할 것입니다.
방금 우리가 만든 것과 같이 모노레포를 이용한다면 VS Code 워크스페이스를 이용하는 것을 추천드립니다. VS Code 워크스페이스를 사용할 경우 각 패키지 별로 구문 강조 표시 및 워크스페이스별 확장 기능을 완전하게 사용할 수 있습니다. VS Code는 프로젝트의 루트에 있는 tsconfig.json
만 고려합니다. 즉, 각 패키지에 있는 tsconfig.json
을 완전히 무시하여 잘못된 문제에 대해서도 긍정을 표시할 수 있습니다. VS Code 워크스페이스는 이런 문제로 부터 여러분들을 도와줍니다.
읽어주셔서 감사합니다.
I used to only look for popular or cool stuff, but reading this post makes me feel a lot.. Thanks! I hope to have more good articles in the future free games unblocked
클린 코드, 클린 아키텍쳐 등등 여러 유명한 서적 읽어도 실제 구현에서 적용하긴 막막하죠. 고전은 시대를 뛰어넘어야 고전인데 코드는 시대에 따라 바뀌는 운명이니까요. 이 글은 고전은 아니겠지만 걸작인 해설서네요. 곧 복잡한 프로젝트를 시작해야하는데 적용해보고 싶네요. 항상 좋은 글 번역해주셔서 감사합니다!