React Clean Architecture - kureharyosuke/Microservices-FrontEnd GitHub Wiki

Sample code of React with Clean architecture

이 ν”„λ‘œμ νŠΈλŠ” ν¬κ²ŒλŠ” μ›Ή μ„œλΉ„μŠ€μ— 클린 μ•„ν‚€ν…μ²˜λ₯Ό λ„μž…ν•˜λŠ”, 그리고 μž‘κ²ŒλŠ” λ¦¬λ•μŠ€ μ•„ν‚€ν…μ²˜μ™€ 클린 μ•„ν‚€ν…μ²˜λ₯Ό ν•¨κ»˜ μ‚¬μš©ν•˜κΈ° μœ„ν•œ, ν•˜λ‚˜μ˜ μž‘μ€ μ•„μ΄λ””μ–΄μ˜ μƒ˜ν”Œ μ½”λ“œμž…λ‹ˆλ‹€.

λΆ€μ‘±ν•œ λΆ€λΆ„μ΄λ‚˜ κ°œμ„ μ‚¬ν•­μ€ Issue λ˜λŠ” Pull Request λ‚¨κ²¨μ£Όμ‹œλ©΄ ν•¨κ»˜ λ°˜μ˜ν•˜λ„λ‘ ν•˜κ² μŠ΅λ‹ˆλ‹€. ☺️

Language

πŸ‡°πŸ‡· πŸ‡ΊπŸ‡²

Use Stack

Typescript, Webpack, React, React-Native, Recoil, Styled-Components

(Recoil > Redux)
https://github.com/falsy/react-with-clean-architecture/tree/v1.8.1

Clean Architecture

image

λ‹€μ–‘ν•œ μ•„ν‚€ν…μ²˜λ“€μ΄ κ·ΈλŸ¬ν•˜λ“― 클린 μ•„ν‚€ν…μ²˜κ°€ κ°–λŠ” 기본의 λͺ©μ  μ—­μ‹œ 관심사λ₯Ό λΆ„λ¦¬ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. 각각의 관심사에 따라 계측을 λ‚˜λˆ„κ³ , μ„ΈλΆ€ κ΅¬ν˜„μ΄ μ•„λ‹Œ 도메인 μ€‘μ‹¬μœΌλ‘œ μ„€κ³„ν•˜λ©°, λ‚΄λΆ€ μ˜μ—­μ΄ ν”„λ ˆμž„μ›Œν¬λ‚˜ λ°μ΄ν„°λ² μ΄μŠ€ UI λ“±μ˜ μ™ΈλΆ€ μš”μ†Œμ— μ˜μ‘΄ν•˜μ§€ μ•Šλ„λ‘ ν•©λ‹ˆλ‹€.

  • μ„ΈλΆ€ κ΅¬ν˜„ μ˜μ—­κ³Ό 도메인 μ˜μ—­μ„ κ΅¬λΆ„ν•©λ‹ˆλ‹€.
  • μ•„ν‚€ν…μ²˜λŠ” ν”„λ ˆμž„μ›Œν¬μ— μ˜μ‘΄ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
  • μ™ΈλΆ€ μ˜μ—­μ€ λ‚΄λΆ€ μ˜μ—­μ— μ˜μ‘΄ν•  수 μžˆμ§€λ§Œ, λ‚΄λΆ€ μ˜μ—­μ€ μ™ΈλΆ€ μ˜μ—­μ— μ˜μ‘΄ν•  수 μ—†μŠ΅λ‹ˆλ‹€.
  • κ³ μˆ˜μ€€, μ €μˆ˜μ€€ λͺ¨λ“ˆ λͺ¨λ‘ 좔상화에 μ˜μ‘΄ν•©λ‹ˆλ‹€.

Communitaction Flow

image κ°„λ‹¨ν•˜κ²Œ λ‹€μ΄μ–΄κ·Έλž¨μœΌλ‘œ ν‘œν˜„ν•˜λ©΄ μœ„μ™€ κ°™μŠ΅λ‹ˆλ‹€.

Session

μ‚¬μš©μž 둜그인 ν›„ λ°œκΈ‰λœ 인증 토큰을 μ›Ή μŠ€ν† λ¦¬μ§€μ— μ €μž₯ν•˜μ—¬ μ‚¬μš©ν•©λ‹ˆλ‹€. μ›Ή μŠ€ν† λ¦¬μ§€λŠ” μ „μ—­μ—μ„œ μ ‘κ·Όν•  수 μžˆμ§€λ§Œ, μƒ˜ν”Œ μ½”λ“œλŠ” μœ„ νλ¦„λŒ€λ‘œ μ§„ν–‰ν•˜μ—¬ 'Infrastructures'의 'Storege'μ—μ„œ μ œμ–΄ν•©λ‹ˆλ‹€. μ΄λŠ” λ³€ν•  수 μžˆλŠ” μ„ΈλΆ€ κ΅¬ν˜„μ˜ 뢀뢄이며, κ·Έ 역할에 맞게 μœ„μΉ˜ν•˜μ—¬ μœ μ§€λ³΄μˆ˜μ— μš©μ˜ν•˜κ²Œ ν•©λ‹ˆλ‹€.

Board

'Infrastructures'μ—μ„œ http 톡신을 톡해 κ²Œμ‹œνŒ κΈ€κ³Ό λŒ“κΈ€μ„ 가져와 'Use Case'μ—μ„œ Comment Entityλ₯Ό ν¬ν•¨ν•œ Board Root Entity둜 μΊ‘μŠν™”ν•˜μ—¬ 'Presenter'둜 μ „λ‹¬ν•˜λ©° 'Presenter'λŠ” Entity 데이터λ₯Ό 'Components'둜 μ „λ‹¬ν•©λ‹ˆλ‹€.
'Components'μ—μ„œλŠ” μƒνƒœ 관리 λ§€λ‹ˆμ €μ— 'Entity' 데이터 λ˜λŠ” 'View Model'둜 μΊ‘μŠν™” ν•œ 데이터λ₯Ό κΈ°μ–΅ν•˜κ³ , λ°μ΄ν„°μ˜ μƒνƒœ 변화에 따라 Viewλ₯Ό λ‹€μ‹œ κ·Έλ¦½λ‹ˆλ‹€.

Inversion of Control

image 'Repository'의 경우 Adapter λ ˆμ΄μ–΄μ— ν•΄λ‹Ήν•˜κΈ° λ•Œλ¬Έμ— 'Use Case'μ—μ„œλŠ” 'Repository'에 λŒ€ν•΄μ„œ μ•Œμ•„μ„œλŠ” μ•ˆλ©λ‹ˆλ‹€. κ·Έλ ‡κΈ° λ•Œλ¬Έμ— 'Use Case'μ—μ„œλŠ” Domain λ ˆμ΄μ–΄ Repository Interfaceλ₯Ό 가지고 κ΅¬ν˜„ν•˜λ©°, μ΄λŠ” 이후에 Dependency Injectionλ₯Ό 톡해 λ™μž‘ν•©λ‹ˆλ‹€.

Directory Structure

./src
β”œβ”€ adapters
β”‚  β”œβ”€ infrastructures
β”‚  β”‚  └─ interfaces
β”‚  β”œβ”€ presenters
β”‚  β”‚  └─ interfaces
β”‚  └─ repositories
β”œβ”€ domains
β”‚  β”œβ”€ aggregates
β”‚  β”‚  └─ interfaces
β”‚  β”œβ”€ entities
β”‚  β”‚  └─ interfaces
β”‚  β”œβ”€ useCases
β”‚  β”‚  β”œβ”€ interfaces
β”‚  β”‚  └─ repository-interfaces
β”‚  └─ dto
└─ frameworks
   β”œβ”€ web
   β”‚  β”œβ”€ di
   β”‚  β”œβ”€ components
   β”‚  β”œβ”€ hooks
   β”‚  └─ vm
   └─ mobile(React Native)
      β”œβ”€ di
      β”œβ”€ components
      β”œβ”€ android
      β”œβ”€ ios
      β”œβ”€ hooks
      └─ vm
  • κΈ°λ³Έ λ””λ ‰ν† λ¦¬λŠ” 클린 μ•„ν‚€ν…μ²˜μ˜ λ ˆμ΄μ–΄λ₯Ό κΈ°μ€€μœΌλ‘œ κ΅¬μ„±ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
    [ frameworks / adapters / domains(useCases / entities) ]
  • μ»΄ν¬λ„ŒνŠΈμ˜ 디렉토리 κ΅¬μ‘°λŠ” μ„œλΉ„μŠ€ λ˜λŠ” ꡬ성원 κ°„ μ•½μ†λœ ν˜•μ‹μœΌλ‘œ 자유둭게 κ΅¬μ„±ν•©λ‹ˆλ‹€.

Screenshots

image image

Alias

Web

tsconfig.json

/src/frameworks/web/tsconfing.json

{
  "compilerOptions": {
    //...
    "baseUrl": ".",
    "paths": {
      "@adapters/*": ["../../adapters/*"],
      "@domains/*": ["../../domains/*"],
      "@frameworks/*": ["../../frameworks/*"],
      "@di": ["./di/index.ts"]
    }
  },
}

webpack.config.js

/src/frameworks/web/webpack.config.js

{
  //...
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
    alias: { 
      "@adapters": path.resolve(__dirname, "../../adapters/"),
      "@domains": path.resolve(__dirname, "../../domains/"),
      "@frameworks": path.resolve(__dirname, "../../frameworks/"),
      "@di": path.resolve(__dirname, "./di/index.ts")
    }
  },
}

Mobile

tsconfig.json

/src/frameworks/mobile/tsconfing.json

{
  "compilerOptions": {
    //...
    "baseUrl": ".",
    "paths": {
      "@adapters/*": ["../../adapters/*"],
      "@domains/*": ["../../domains/*"],
      "@frameworks/*": ["../../frameworks/*"],
      "@di": ["./di/index.ts"]
    }
  },
}

metro.config.js

/src/frameworks/mobile/metro.config.js

const path = require('path')
const extraNodeModules = {
  '@adapters': path.resolve(__dirname + './../../adapters'),
  '@domains': path.resolve(__dirname + './../../domains'),
  '@frameworks': path.resolve(__dirname + './../../frameworks'),
}
const watchFolders = [
  path.resolve(__dirname + './../../adapters'),
  path.resolve(__dirname + './../../domains'),
  path.resolve(__dirname + './../../frameworks'),
]

module.exports = {
  //...
  resolver: {
    extraNodeModules: new Proxy(extraNodeModules, {
      get: (target, name) =>
        name in target ? target[name] : path.join(process.cwd(), `node_modules/${name}`),
    }),
  },
  watchFolders,
}

Run Project

1. Mock Server

Install

# $ cd /mock-server
$ npm install

Start

# $ cd /mock-server
$ npm start

2-1. Web

Install

# $ cd /src/frameworks/web
$ npm install

Start

# $ cd /src/frameworks/web
$ npm start

2-2. Mobile(ios)

Install

# $ cd /src/frameworks/mobile
$ npm install

# cocoapods install
$ gem install cocoapods

# $ cd /src/frameworks/mobile/ios
$ pod install

Start

# $ cd /src/frameworks/mobile
$ npx react-native run-ios

Version

v1.9.0 - ChangeLog

https://github.com/falsy/react-with-clean-architecture/blob/master/readme-ko.md