Todo 앱에 TDD 적용해보기(2)
이전에 Todo 앱의 요구사항을 정의하고 간단한 디자인을 만들어봤다. 이제 이 요구사항을 바탕으로 앱을 만들어보자. 이번 포스트에서는 앱 초기 설정을 진행해 볼 것이다.
이전에 Todo 앱의 요구사항을 정의하고 간단한 디자인을 만들어봤다. 이제 이 요구사항을 바탕으로 앱을 만들어보자. 이번 포스트에서는 앱 초기 설정을 진행해 볼 것이다.
디자인은 수정할 예정이다. 여기서 중요한 건, 디자인을 수정하는 것과 이전에 정의했던 요구사항 중 도메인과 관련된 요구사항은 별개로 디자인 수정이 도메인 요구사항에 영향을 주면 안 된다. 디자인은 도메인 요구사항을 사용자에게 보여주기 위한 도구이기 때문인다.
💻 앱 초기 설정하기
우선 vite를 이용해서 react 앱을 생성한다.
npm create vite@latest
https://ko.vite.dev/guide/#scaffolding-your-first-vite-project
위 공식 문서를 읽어보면 쉽게 리액트 앱을 만들 수 있다.
다음으로 초기 폴더 구조를 다음과 같은 형태로 만들었다.
. 📂 vite-todo
└── 📂 src/
│ ├── 📄 App.css
│ ├── 📄 App.tsx
│ ├── 📄 index.css
│ ├── 📄 main.tsx
│ ├── 📄 vite-env.d.ts
├── 📄 README.md
├── 📄 index.html
├── 📄 package-lock.json
├── 📄 package.json
├── 📄 tsconfig.app.json
├── 📄 tsconfig.json
├── 📄 tsconfig.node.json
└── 📄 vite.config.ts
📁 FSD 구조 적용하기
이번 앱을 만들 때는 FSD 구조를 적용해볼 생각이다. FSD는 기능 기반으로 설계된 프론트엔드 폴더 구조 형태이고, 커스텀이 쉬운 리액트 앱 아키텍처에 기준을 만들어 줄 수 있을 것 같아 적용해 볼 것 이다.
실제로 내가 이번에 앱을 개발하려는 방식과 잘 어울리는 폴더 구조일 것이라고 생각한다.
우선 FSD 구조에 필요한 경로 설정을 해보자.
tsconfig 파일에 다음과 같이 path alias를 설정한다.
"paths": {
"@app/*": ["src/app/*"],
"@entities/*": ["src/entities/*"],
"@features/*": ["src/features/*"],
"@shared/*": ["src/shared/*"],
"@pages/*": ["src/pages/*"],
"@widgets/*": ["src/widgets/*"]
}
이제 타입스크립트가 컴파일 시 경로 별칭을 사용하여 컴파일하게 된다.
하지만, 아직 build 및 런타임 시 별칭이 설정되지 않았다. 이는 vite 설정에서 해주면 된다.
// vite.config.ts
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@app': path.resolve(__dirname, 'src/app'),
'@entities': path.resolve(__dirname, 'src/entities'),
'@features': path.resolve(__dirname, 'src/features'),
'@shared': path.resolve(__dirname, 'src/shared'),
'@widgets': path.resolve(__dirname, 'src/widgets'),
'@pages': path.resolve(__dirname, 'src/pages'),
},
},
});
이제 각 레이어에 맞게 폴더를 생성하고 App.tsx 파일을 app폴더 하위로 이동시켜보자.
그랬더니 main.tsx에서 이렇게 파일을 불러올 수 있게 되었다.
import App from '@app/App'; // path alias에 맞게 파일을 불러온다.
import React from 'react';
import ReactDOM from 'react-dom/client';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
그런데 import 순서가 좀 아쉽다. 그래서 eslint도 수정헀다.
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:import/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh', 'import'],
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'import/no-duplicates': 'error',
'import/no-unresolved': 0,
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'unknown'],
pathGroups: [
{
pattern: 'react',
group: 'external',
position: 'before',
},
{
pattern: 'react-dom/**',
group: 'external',
position: 'before',
},
{
pattern: 'react-router-dom',
group: 'external',
position: 'before',
},
{
pattern: '@app/**',
group: 'internal',
position: 'after',
},
{
pattern: '@pages/**',
group: 'internal',
position: 'after',
},
{
pattern: '@shared/**',
group: 'internal',
position: 'after',
},
{
pattern: '@widgets/**',
group: 'internal',
position: 'after',
},
{
pattern: '@features/**',
group: 'internal',
position: 'after',
},
],
pathGroupsExcludedImportTypes: ['builtin'],
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
'newlines-between': 'always',
},
],
},
};
이제 확인해보면,
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from '@app/App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
나쁘지 않다.
🤔 다음으로…
이제 다음 포스트에서는 요구사항을 기반으로 도메인 모델링을 해볼 예정이다. 도메인 모델링에 대한 포스팅이 있으니 참고해보면 좋겠다.