
π― λ°±μλ - νλ‘ νΈμλ λλ ν 리 ꡬ쑰λ₯Ό μ€κ³νκ³ ν μ€νΈ λ°μ΄ν°λ² μ΄μ€λ₯Ό ꡬμ±ν©λλ€.
π 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π€ Poolκ³Ό DAOλ μ΄λ€ λ»μΌκΉ?
Pool (DB Pool)
DB μ°κ²°μ 미리 μ¬λ¬ κ° λ§λ€μ΄λκ³ νμν λλ§λ€ νλμ© κΊΌλ΄μ°λ ꡬ쑰DAO (Data Access Object)
λ°μ΄ν°λ² μ΄μ€μ μ§μ μ κ·Όν΄μ CRUD νλ ν΄λμ€λ κ°μ²΄
user.ts
μ¬μ©μμ λν CRUDλ₯Ό μ 곡νλ λͺ¨λΈ ν΄λμ€μ λλ€.
BCryptλ‘ λ¨λ°©ν₯μνΈνλ λΉλ°λ²νΈλ₯Ό νλ¬Έ λΉλ°λ²νΈμ λμ‘°νλ μΈμ€ν΄μ€ λ©μλλ₯Ό μ 곡ν©λλ€.
utils/mysql.ts
middlewares/authentication.ts
authorization.ts
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.tsimport dotenv from "dotenv";
dotenv.config();
export const PORT = process.env.PORT || 3031;
src/app.tsimport 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.tsimport { app } from './app';
import { PORT } from './setting';
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
π 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/ # μμ΄μ½ λ° μ΄λ―Έμ§ λͺ¨μ
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 λ₯Ό μ°κ²°νλ©΄μ μ 체 νλ¦μ μ§μ ꡬμ±ν΄λ³΄λ κ°λ μ΄ λ μ μ‘νλ κ² κ°λ€. νμ§λ§ λ€μ λΉ λ₯Έ μ§νκ³Ό λ§μ μ 보λμ μ μ΄ν΄νκ³ μλμ§ κ³μ μκ°νλ©΄μ μ½λλ₯Ό μ§μΌν κ² κ°λ€.