🎯 λ°±μ—”λ“œ - ν”„λ‘ νŠΈμ—”λ“œ 디렉토리 ꡬ쑰λ₯Ό μ„€κ³„ν•˜κ³  ν…ŒμŠ€νŠΈ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό κ΅¬μ„±ν•©λ‹ˆλ‹€.


πŸ“— Today I Learned

Backend

Backend 파일 ꡬ쑰

πŸ“ src
β”œβ”€β”€ πŸ“„ index.ts        # μ•± μ§„μž…μ  파일
β”œβ”€β”€ πŸ“„ app.ts          # μ•± μ§„μž…μ  파일
β”œβ”€β”€ πŸ“„ settings.ts     # ν™˜κ²½λ³€μˆ˜ 및 각쒅 μ„€μ • λ³€μˆ˜
β”œβ”€β”€ πŸ“ routes/         # 라우트 ν•¨μˆ˜ λͺ¨μŒ
β”‚    β”œβ”€β”€ πŸ“„ users.ts
β”‚    β”œβ”€β”€ πŸ“„ notes.ts 
β”‚    └── πŸ“„ healthcheck.ts 
β”œβ”€β”€ πŸ“ models/         # λͺ¨λΈ 클래슀 λͺ¨μŒ
β”‚    β”œβ”€β”€ πŸ“„ note.ts 
β”‚    └── πŸ“„ user.ts 
β”œβ”€β”€ πŸ“ utils/          # μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜ 및 μœ ν‹Έλ¦¬ν‹° 객체 λͺ¨μŒ
β”‚    └── πŸ“„ mysql.ts 
└── πŸ“ middlewares/    # 미듀웨어 ν•¨μˆ˜ λͺ¨μŒ
     β”œβ”€β”€ πŸ“„ authentication.ts 
     └── πŸ“„ authorization.ts 

πŸ“ routes/

  • users.ts

    • Base 경둜 : /

    • 둜그인 및 JWT μΏ ν‚€ λ°œκΈ‰ : POST /login

    • λ‘œκ·Έμ•„μ›ƒ 및 μΏ ν‚€ μ‚­μ œ : POST /logout

    • μ‚¬μš©μž 본인 정보 쑰회 : GET /users/me

    • νšŒμ›κ°€μž… : POST /users


  • notes.ts

    • Base 경둜 : /notes

    • λ…ΈνŠΈ λͺ©λ‘ 쑰회 : GET /

    • λ…ΈνŠΈ 상세 쑰회 : GET /:id

    • λ…ΈνŠΈ 생성 : POST /

    • λ…ΈνŠΈ μˆ˜μ • : PUT /:id

    • λ…ΈνŠΈ μ‚­μ œ : DELETE /:id


  • healthcheck.ts

    • Base 경둜 : /healthcheck

    • Docker ν—¬μŠ€μ²΄ν¬λ₯Ό μœ„ν•œ 204 응닡 : GET /


πŸ“ models/

DB Pool 객체λ₯Ό 톡해 λͺ¨λΈμ„ CRUDν•  수 μžˆλŠ” DAO역할을 ν•©λ‹ˆλ‹€.

  • note.ts
    • λ…ΈνŠΈμ— λŒ€ν•œ CRUDλ₯Ό μ œκ³΅ν•˜λŠ” λͺ¨λΈ ν΄λž˜μŠ€μž…λ‹ˆλ‹€.

πŸ€” Poolκ³Ό DAOλŠ” μ–΄λ–€ 뜻일까?

Pool (DB Pool)
DB 연결을 미리 μ—¬λŸ¬ 개 λ§Œλ“€μ–΄λ†“κ³  ν•„μš”ν•  λ•Œλ§ˆλ‹€ ν•˜λ‚˜μ”© κΊΌλ‚΄μ“°λŠ” ꡬ쑰

DAO (Data Access Object)
λ°μ΄ν„°λ² μ΄μŠ€μ— 직접 μ ‘κ·Όν•΄μ„œ CRUD ν•˜λŠ” ν΄λž˜μŠ€λ‚˜ 객체


  • user.ts

    • μ‚¬μš©μžμ— λŒ€ν•œ CRUDλ₯Ό μ œκ³΅ν•˜λŠ” λͺ¨λΈ ν΄λž˜μŠ€μž…λ‹ˆλ‹€.

    • BCrypt둜 단방ν–₯μ•”ν˜Έν™”λœ λΉ„λ°€λ²„ν˜Έλ₯Ό 평문 λΉ„λ°€λ²ˆν˜Έμ™€ λŒ€μ‘°ν•˜λŠ” μΈμŠ€ν„΄μŠ€ λ©”μ„œλ“œλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.


πŸ“ utils/

  • mysql.ts

    • DB SQL 쿼리λ₯Ό μœ„ν•œ MySQL Pool μΈμŠ€ν„΄μŠ€λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.

πŸ“ middlewares/

  • authentication.ts

    • Request 객체의 μΏ ν‚€λ‘œ μ£Όμ–΄μ§„ JWTλ₯Ό κ²€μ¦ν•˜κ³ , User DAOλ₯Ό Request 객체에 μΆ”κ°€ν•©λ‹ˆλ‹€.

  • authorization.ts

    • Request 객체의 id param에 ν•΄λ‹Ήν•˜λŠ” Noteκ°€ μ‘΄μž¬ν•˜λŠ”μ§€ ν™•μΈν•˜κ³ , Note의 ownerκ°€ Request 객체의 User와 μΌμΉ˜ν•˜λŠ”μ§€ κ²€μ¦ν•˜κ³ , Note DAOλ₯Ό Request 객체에 μΆ”κ°€ν•©λ‹ˆλ‹€.



λ°μ΄ν„°λ² μ΄μŠ€ 섀계

  • init-user.sql : DB μ‚¬μš©μž 생성 및 κΆŒν•œ μ„€μ •
USE mysql;

-- μ‚¬μš©μž 생성 (둜컬 μ „μš©)
CREATE USER IF NOT EXISTS 'prgms'@'localhost' IDENTIFIED BY 'λΉ„λ°€λ²ˆν˜Έ';

-- μ‚¬μš©μž 생성 (λͺ¨λ“  IP ν—ˆμš© β€” 개발용)
CREATE USER IF NOT EXISTS 'prgms'@'%' IDENTIFIED BY 'λΉ„λ°€λ²ˆν˜Έ';

-- νŠΉμ • DB에 λŒ€ν•œ κΆŒν•œ λΆ€μ—¬
GRANT ALL PRIVILEGES ON prgms_notes.* TO 'prgms'@'localhost';
GRANT ALL PRIVILEGES ON prgms_notes.* TO 'prgms'@'%';

-- κΆŒν•œ 변경사항 적용
FLUSH PRIVILEGES;

  • init-db.sql : DB 및 ν…Œμ΄λΈ” 생성
CREATE SCHEMA IF NOT EXISTS `prgms_notes` DEFAULT CHARACTER SET utf8mb4;
USE `prgms_notes`

-- μ‚¬μš©μž ν…Œμ΄λΈ” 생성
CREATE TABLE IF NOT EXISTS users (
    id INT NOT NULL AUTO_INCREMENT,
    email VARCHAR(256) NOT NULL,
    encrypted_password text NOT NULL,
    PRIMARY KEY (id),
    UNIQUE INDEX users_unique_email (email) USING BTREE
);

-- λ…ΈνŠΈ ν…Œμ΄λΈ” 생성
CREATE TABLE IF NOT EXISTS notes (
    id INT NOT NULL AUTO_INCREMENT,
    title text NOT NULL,
    content text NOT NULL,
    user_id INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    CONSTRAINT note_user_id
        FOREIGN KEY (user_id)
        REFERENCES users (id)
        ON DELETE SET NULL
        ON UPDATE CASCADE
);

  • init-test-db.sql : ν…ŒμŠ€νŠΈμš© 초기 데이터 μ‚½μž…
DROP DATABASE IF EXISTS `prgms_notes`;
CREATE SCHEMA `prgms_notes` DEFAULT CHARACTER SET utf8mb4;
USE `prgms_notes`

--
-- Table structure for table `users`
--

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `email` varchar(256) NOT NULL,
  `encrypted_password` text NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `users_unique_email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

--
-- Table structure for table `notes`
--

CREATE TABLE `notes` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` text NOT NULL,
  `content` text NOT NULL,
  `user_id` int(11) DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT current_timestamp(),
  `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
  PRIMARY KEY (`id`),
  KEY `note_user_id` (`user_id`),
  CONSTRAINT `note_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

--
-- Dumping data for table `users`
--

LOCK TABLES `users` WRITE;
INSERT INTO `users` VALUES (1,'test@example.com','$2b$10$432oW5OwXPcHPcmyQghkxeICMi65DGPdFnDv21dJ2QU3CSj.xFbi6');
UNLOCK TABLES;

--
-- Dumping data for table `notes`
--

LOCK TABLES `notes` WRITE;
INSERT INTO `notes` VALUES (1,'Test (1)','<p>This note is for testing.</p><p>Note number: 1</p>',1,'2024-01-24 05:47:47','2024-01-24 05:48:04'),(2,'Test (2)','<p>This note is for testing.</p><p>Note number: 2</p>',1,'2024-01-24 05:48:08','2024-01-24 05:48:23');
UNLOCK TABLES;



ν…ŒμŠ€νŠΈ λ°μ΄ν„°λ² μ΄μŠ€ μ„€μΉ˜

  • notes-db.yaml : DB μ»¨ν…Œμ΄λ„ˆ μ •μ˜ 및 μ„œλΉ„μŠ€ μ„€μ •
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: notes-db
  namespace: db
spec:
  ...
  template:
    spec:
      containers:
        - name: notes-database
          image: mariadb:11.2.2
          env:
            - name: MARIADB_ROOT_PASSWORD
              value: root
          volumeMounts:
            - name: notes-db-storage
              mountPath: "/var/lib/mysql"
      volumes:
        - name: notes-db-storage
          persistentVolumeClaim:
            claimName: notes-db-pvc

---

# Service
apiVersion: v1
kind: Service
metadata:
  name: notes-db
  labels:
    run: notes-db
  namespace: db
spec:
  type: NodePort
  selector:
    run: notes-db
  ports:
    - port: 3306
      nodePort: 30036            

  • notes-db-volume.yaml : PersistentVolume / PVC μ •μ˜
# PersistentVolume
apiVersion: v1
kind: PersistentVolume
metadata:
  name: notes-db-volume
  labels:
    type: local
  namespace: db
spec:
  storageClassName: manual
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "<Path to your data directory>"

---

# PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: notes-db-pvc
  namespace: db
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
kubectl create namespace db
kubectl apply -f notes-db-volume.yaml
kubectl apply -f notes-db.yaml

μ ‘κ·Ό 방법

  • 둜컬: localhost:30036

  • ν΄λŸ¬μŠ€ν„° λ‚΄λΆ€: notes-db.db.svc.cluster.local

  • μ΄ˆκΈ°ν™” λͺ…λ Ήμ–΄

mysql --protocol tcp -P 30036 -u root -p < init-user.sql # 루트 κ³„μ •μœΌλ‘œ μ‚¬μš©μž 생성
mysql --protocol tcp -P 30036 -u root -p < init-db.sql   # 루트 κ³„μ •μœΌλ‘œ DB 및 ν…Œμ΄λΈ” 생성
mysql --protocol tcp -P 30036 -u prgms -p                # prgms κ³„μ •μœΌλ‘œ DB μ ‘κ·Ό ν…ŒμŠ€νŠΈ



λ°±μ—”λ“œ 디렉토리 μ΄ˆκΈ°ν™”

npm init -y                                  # package.json μ΄ˆκΈ°ν™”
npm i dotenv express express-async-errors    # λŸ°νƒ€μž„ μ˜μ‘΄μ„± μ„€μΉ˜
npm i -D typescript @types/express nodemon   # 개발 μ˜μ‘΄μ„± μ„€μΉ˜

  • package.json
...
  "main": "build/index.js",
  "scripts": {
    "start": "nodemon",
    "build": "tsc",
   }
...

  • nodemon.json
{
  "$schema": "https://json.schemastore.org/nodemon.json",
  "watch": ["src"],
  "ignore": ["src/**/*.test.ts"],
  "ext": "ts,json",
  "exec": "tsc && node .",
  "legacyWatch": true
}

  • tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "CommonJS",
    "moduleResolution": "node",
    "isolatedModules": true,
    "outDir": "./build"
  },
  "include": ["src"],
  "exclude": ["src/**/*.test.ts", "src/**/__mocks__/*.ts"]
}

  • src/settings.ts
import dotenv from "dotenv";

dotenv.config();
export const PORT = process.env.PORT || 3031;

  • src/app.ts
import express, { NextFunction, Request, Response } from 'express';
import 'express-async-errors';

const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
  console.error(err);
  res.sendStatus(500);
});

export { app };

  • src/index.ts
import { app } from './app';
import { PORT } from './setting';

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});



Frontend

Frontend 파일 ꡬ쑰

πŸ“ src
β”œβ”€β”€ πŸ“„ index.tsx        # 슀크립트 μ§„μž…μ  파일 
β”œβ”€β”€ πŸ“„ App.tsx          #  메인 μ•± μ»΄ν¬λ„ŒνŠΈ
β”œβ”€β”€ πŸ“„ App.css          # 메인 μŠ€νƒ€μΌμ‹œνŠΈ
β”œβ”€β”€ πŸ“„ router.tsx       # λΌμš°ν„° 파일
β”œβ”€β”€ πŸ“„ settings.ts      # ν™˜κ²½λ³€μˆ˜ 파일
β”œβ”€β”€ πŸ“ pages/           # νŽ˜μ΄μ§€ μ»΄ν¬λ„ŒνŠΈ λͺ¨μŒ
β”‚    β”œβ”€β”€ πŸ“„ Index.tsx 
β”‚    β”œβ”€β”€ πŸ“„ Login.tsx 
β”‚    β”œβ”€β”€ πŸ“„ Join.tsx 
β”‚    β”œβ”€β”€ πŸ“„ Error.tsx 
β”‚    β”œβ”€β”€ πŸ“„ Index.template.tsx 
β”‚    └── πŸ“ notes/
β”‚         β”œβ”€β”€ πŸ“„ Index.tsx 
β”‚         └── πŸ“„ Detail.tsx 
β”œβ”€β”€ πŸ“ components/      # μ„ΈλΆ€ μ»΄ν¬λ„ŒνŠΈ λͺ¨μŒ
β”‚    β”œβ”€β”€ πŸ“„ LoginForm.ts 
β”‚    β”œβ”€β”€ πŸ“„ JoinForm.ts 
β”‚    β”œβ”€β”€ πŸ“„ NoteList.ts 
β”‚    β”œβ”€β”€ πŸ“„ NoteTitleInput.ts 
β”‚    β”œβ”€β”€ πŸ“„ NoteContentEditor.ts
β”‚    β”œβ”€β”€ πŸ“ boundaries/
β”‚    β”‚    β”œβ”€β”€ πŸ“„ AppErrorBoundary.tsx 
β”‚    β”‚    └── πŸ“„ RouteErrorBoundary.tsx 
β”‚    └── πŸ“ hocs/
β”‚         β”œβ”€β”€ πŸ“„ withUnauthenticated.tsx 
β”‚         └── πŸ“„ withAuthenticatedUser.tsx 
β”‚         └── πŸ“„ withCurrentNote.tsx 
β”œβ”€β”€ πŸ“ hooks/           # React Query μƒνƒœκ΄€λ¦¬ κ΄€λ ¨ Hook λͺ¨μŒ
β”œβ”€β”€ πŸ“ apis/            # API 호좜 ν•¨μˆ˜ λͺ¨μŒ
β”œβ”€β”€ πŸ“ types/           # λͺ¨λΈ νƒ€μž… λͺ¨μŒ
β”œβ”€β”€ πŸ“ utils/           # μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜ 및 객체 λͺ¨μŒ
β”‚    β”œβ”€β”€ πŸ“„ http.ts 
β”‚    └── πŸ“„ getStatusFormError.ts 
└── πŸ“ assets/          # μ•„μ΄μ½˜ 및 이미지 λͺ¨μŒ

CRA ν…œν”Œλ¦Ώ 및 μ„€μ •

npx create-react-app frontend --template typescript

npm i @craco/craco tsconfig-paths-webpack-plugin react-router-dom @tanstack/react-query styled-components open-color
  • craco.config.js : 경둜 alias μ„€μ •
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");

/** @type {import('webpack').Configuration} */
module.exports = {
  plugins: [
    {
      plugin: {
        overrideWebpackConfig: ({ webpackConfig }) => {
          webpackConfig.resolve.plugins.push(new TsconfigPathsPlugin({}));
          return webpackConfig;
        },
      },
    },
  ],
};
  • Index.template.tsx : 첫 ν™”λ©΄ ν…œν”Œλ¦Ώ ꡬ성 μ»΄ν¬λ„ŒνŠΈ
import { Link } from "react-router-dom";
import { styled } from "styled-components";
import oc from "open-color";
import { ReactComponent as MussgImage } from "@/assets/mussg.svg";
export const IndexTemplate: React.FC = () => {
 return (
 <Container>
   <MussgImage height="182" />
   <AppTitle>Programmers Note Editor</AppTitle>
   <AppDescription>
     <strong>Programmers Note Editor</strong>λŠ” (...)
     <br />
     λ©”λͺ¨λŠ” ν΄λΌμš°λ“œμ— μ €μž₯λ˜μ–΄ μ–Έμ œ μ–΄λ””μ„œλ‚˜ (...)
   </AppDescription>
   <StartLink to="login">무료둜 μ‹œμž‘ν•˜κΈ°</StartLink>
   <Footer>Β© 2023 Grepp Co.</Footer>
 </Container>
 );
};



✏️ 회고

DB - FE - BE λ₯Ό μ—°κ²°ν•˜λ©΄μ„œ 전체 흐름을 직접 κ΅¬μ„±ν•΄λ³΄λ‹ˆ κ°œλ…μ΄ 더 잘 μž‘νžˆλŠ” 것 κ°™λ‹€. ν•˜μ§€λ§Œ λ‹€μ†Œ λΉ λ₯Έ μ§„ν–‰κ³Ό λ§Žμ€ μ •λ³΄λŸ‰μ— 잘 μ΄ν•΄ν•˜κ³  μžˆλŠ”μ§€ 계속 μƒκ°ν•˜λ©΄μ„œ μ½”λ“œλ₯Ό μ§œμ•Όν•  것 κ°™λ‹€.

profile
🌱개발 기둝μž₯

0개의 λŒ“κΈ€