์ด ๋ฌธ์์์๋ Altsis ํ๋ก์ ํธ์์ ๋ฐ๋ฅด๋ ์ฝ๋ฉ ์คํ์ผ๊ณผ ๊ท์น์ ์ ์ํฉ๋๋ค. ์ผ๊ด๋ ์ฝ๋ ์คํ์ผ์ ๊ฐ๋ ์ฑ์ ๋์ด๊ณ ํ์ ์ ์ํํ๊ฒ ํฉ๋๋ค.
- ์ผ๋ฐ ๊ท์น
- ๋ช ๋ช ๊ท์น
- TypeScript ๊ท์น
- React ์ปดํฌ๋ํธ ๊ท์น
- SCSS ๊ท์น
- ๋ฐฑ์๋ JavaScript ๊ท์น
- Prettier ์ค์
- Linter ์ค์
- ์ฐธ๊ณ ์๋ฃ
| ํ์ผ ์ ํ | ํ์ฅ์ | ์ฌ์ฉ ์์ |
|---|---|---|
| React ์ปดํฌ๋ํธ | .tsx |
JSX๋ฅผ ํฌํจํ๋ ํ์ผ |
| TypeScript ๋ชจ๋ | .ts |
JSX ์๋ ์ ํธ/ํ์ /ํ |
| ์คํ์ผ | .module.scss |
์ปดํฌ๋ํธ ์ค์ฝํ ์คํ์ผ |
| ์ ์ญ ์คํ์ผ | .scss |
์ ์ญ ์คํ์ผ (variables, global ๋ฑ) |
| ๋ฐฑ์๋ | .js |
ES Module ๋ฌธ๋ฒ ์ฌ์ฉ |
// ํ์ดํ ํจ์ ์ฌ์ฉ (์ปดํฌ๋ํธ, HTML ๋ฐํ ํจ์)
const MyComponent = () => {
return <div>๋ด์ฉ</div>;
};
// ๋น๊ตฌ์กฐํ ํ ๋น
const { currentUser, currentSchool } = useAuth();
// ํ
ํ๋ฆฟ ๋ฆฌํฐ๋ด
const message = `${user.name}๋, ํ์ํฉ๋๋ค.`;
// Optional Chaining
const schoolName = currentUser?.schools?.[0]?.schoolName;
// Nullish Coalescing
const name = user.name ?? "์ด๋ฆ ์์";// 1. ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import _ from "lodash";
// 2. ๋ด๋ถ ์ปดํฌ๋ํธ/๋ชจ๋
import Table from "components/tableV2/Table";
import Loading from "components/loading/Loading";
// 3. Context / Hooks
import { useAuth } from "contexts/authContext";
import useAPIv2 from "hooks/useAPIv2";
// 4. ํ์
import { TSeason } from "types/seasons";
// 5. ์คํ์ผ
import style from "./myComponent.module.scss";| ๋์ | ๊ท์น | ์์ |
|---|---|---|
| ๋ณ์ | camelCase | currentUser, seasonList, isLoading |
| ํจ์ | camelCase | loadData, handleSubmit, formatDate |
| ์์ | SCREAMING_SNAKE_CASE | PERMISSION_DENIED, MAX_FILE_SIZE |
| ๋ถ๋ฆฌ์ธ ๋ณ์ | is/has/can ์ ๋์ฌ |
isActive, hasPermission, canEdit |
| ๋์ | ๊ท์น | ์์ |
|---|---|---|
| React ์ปดํฌ๋ํธ | PascalCase | MyComponent, CourseList, EventPopup |
| ํ์ด์ง ์ปดํฌ๋ํธ | PascalCase | Index, Pid, Design |
| ์ปดํฌ๋ํธ ํ์ผ๋ช | PascalCase | MyComponent.tsx, EventPopup.tsx |
| ์คํ์ผ ํ์ผ๋ช | camelCase | myComponent.module.scss |
์ด๋ฒคํธ ํธ๋ค๋ฌ๋ handle ์ ๋์ฌ๋ฅผ ์ฌ์ฉํฉ๋๋ค:
// ์ฌ๋ฐ๋ฅธ ์์
const handleOnClick = () => { ... };
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... };
const handleSubmit = () => { ... };
const handleDelete = (id: string) => { ... };
// ์๋ชป๋ ์์
const onClick = () => { ... }; // handle ์ ๋์ฌ ๋๋ฝ
const deleteItem = () => { ... }; // handle ์ ๋์ฌ ๋๋ฝ| ๋์ | ๊ท์น | ์์ |
|---|---|---|
| ์ปจํธ๋กค๋ฌ ํจ์ | C/R/U/D + ๋ฆฌ์์ค๋ช | CSeason, RSeasons, USeason, DSeason |
| ๋ผ์ฐํธ ํ์ผ | ์๋ฌธ์ ๋ณต์ํ | seasons.js, syllabuses.js |
| ๋ชจ๋ธ ํ์ผ | PascalCase ๋จ์ํ | Season.js, Syllabus.js |
| ๋ชจ๋ธ ๋ด๋ณด๋ด๊ธฐ | PascalCase ๋จ์ํ | export const Season = (dbName) => {...} |
| ์๋น์ค | camelCase | SeasonService, addSeasonPermissionException |
| ์ ๋์ฌ | ์ฉ๋ | ์์ |
|---|---|---|
I |
์ธํฐํ์ด์ค | IMyComponentProps, IDatabaseQuery |
T |
ํ์ ๋ณ์นญ | TUser, TSeason, TEnrollment |
// ์ธํฐํ์ด์ค - I ์ ๋์ฌ
interface IMyComponentProps {
title: string;
data: TSeason[];
onSelect: (season: TSeason) => void;
}
// ํ์
๋ณ์นญ - T ์ ๋์ฌ
type TUser = {
_id: string;
userId: string;
userName: string;
auth: "owner" | "admin" | "manager" | "teacher" | "student";
};
type TCurrentUser = TUser & {
academyId: string;
academyName: string;
schools: Array<{
school: string;
schoolId: string;
schoolName: string;
}>;
};| ์์น | ์ฉ๋ |
|---|---|
frontend/src/types/*.ts |
๋๋ฉ์ธ ๋ชจ๋ธ ํ์ (API ์๋ต ๋ฐ์ดํฐ) |
| ์ปดํฌ๋ํธ ํ์ผ ๋ด๋ถ | ํด๋น ์ปดํฌ๋ํธ ์ ์ฉ Props/State ํ์ |
frontend/src/global.d.ts |
์ ์ญ ํ์ ์ ์ธ |
// types/seasons.ts
export type TSeason = {
_id: string;
school: string;
schoolId: string;
schoolName: string;
year: string;
term: string;
period: {
start: string;
end: string;
};
isActivated: boolean;
};
// ์ปดํฌ๋ํธ์์ ์ฌ์ฉ
import { TSeason } from "types/seasons";
interface ISeasonListProps {
seasons: TSeason[];
onSelect: (season: TSeason) => void;
}
const SeasonList = ({ seasons, onSelect }: ISeasonListProps) => {
return (
<ul>
{seasons.map((season) => (
<li key={season._id} onClick={() => onSelect(season)}>
{season.year} {season.term}
</li>
))}
</ul>
);
};ํ์ดํ ํจ์๋ฅผ ์ฌ์ฉํฉ๋๋ค:
// ์ฌ๋ฐ๋ฅธ ์์ - ํ์ดํ ํจ์
const MyComponent = ({ title, data }: IMyComponentProps) => {
return (
<div className={style.container}>
<h1>{title}</h1>
</div>
);
};
export default MyComponent;const MyComponent = () => {
// 1. Context ํ
const { currentUser } = useAuth();
const { SeasonAPI } = useAPIv2();
// 2. State ์ ์ธ
const [seasons, setSeasons] = useState<TSeason[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isPopupOpen, setIsPopupOpen] = useState(false);
// 3. Effect ํ
useEffect(() => {
loadData();
}, []);
// 4. ์ด๋ฒคํธ ํธ๋ค๋ฌ
const handleOnClick = (season: TSeason) => {
// ...
};
// 5. ๋ฐ์ดํฐ ๋ก๋ ํจ์
const loadData = async () => {
try {
setIsLoading(true);
const { seasons } = await SeasonAPI.RSeasons();
setSeasons(seasons);
} catch (err) {
ALERT_ERROR(err);
} finally {
setIsLoading(false);
}
};
// 6. ์กฐ๊ฑด๋ถ ๋ ๋๋ง
if (isLoading) return <Loading />;
// 7. JSX ๋ฐํ
return (
<div className={style.container}>
{/* ... */}
</div>
);
};// ์ผํญ ์ฐ์ฐ์ (์งง์ ๊ฒฝ์ฐ)
{isActive ? <ActiveBadge /> : <InactiveBadge />}
// && ์ฐ์ฐ์ (ํ์ชฝ๋ง ๋ ๋๋ง)
{isPopupOpen && <MyPopup setState={setIsPopupOpen} />}
// ์กฐ๊ธฐ ๋ฐํ (์ ์ฒด ์ปดํฌ๋ํธ)
if (isLoading) return <Loading />;
if (!data) return <div>๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.</div>;// ์ฌ๋ฐ๋ฅธ ์์
.container {
background-color: var(--background-color);
color: var(--accent-1);
border: var(--border-default);
box-shadow: var(--component-box-shadow);
}
.button {
background-color: var(--btn-color-1);
color: var(--btn-text-color-1);
&:hover {
background-color: var(--background-hover-color);
}
}
// ์๋ชป๋ ์์ - ํ๋์ฝ๋ฉ ์ ๋ ๊ธ์ง
.container {
background-color: #ffffff;
color: black;
border: 1px solid #e6e6e6;
}// ํด๋์ค๋ช
: camelCase
.container { }
.headerTitle { }
.actionButton { }
.listItem { }
.emptyMessage { }
// BEM ์คํ์ผ๋ ํ์ฉ
.card { }
.card__header { }
.card__body { }
.card--active { }.container {
display: flex;
flex-direction: column;
gap: 16px;
.header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
font-size: 18px;
font-weight: 600;
color: var(--accent-1);
}
}
.content {
padding: 16px;
background-color: var(--component-color);
border-radius: 8px;
}
}๋ฐฑ์๋๋ "type": "module"๋ก ES Module์ ์ฌ์ฉํฉ๋๋ค:
// ์ฌ๋ฐ๋ฅธ ์์ - ES Module
import express from "express";
import { Season, School } from "../models/index.js";
export const create = async (req, res) => { ... };
export { router };
// ์๋ชป๋ ์์ - CommonJS (์ฌ์ฉ ๊ธ์ง)
const express = require("express");
module.exports = { create };์ฃผ์: import ๊ฒฝ๋ก์
.jsํ์ฅ์๋ฅผ ๋ฐ๋์ ํฌํจํด์ผ ํฉ๋๋ค.
// async/await ์ฌ์ฉ (๊ถ์ฅ)
export const find = async (req, res) => {
try {
const seasons = await Season(req.user.academyId).find({});
return res.status(200).send({ seasons });
} catch (err) {
return res.status(500).send({ message: err.message });
}
};๋ชจ๋ ์ปจํธ๋กค๋ฌ ํจ์์ JSDoc ์ฃผ์์ ์์ฑํฉ๋๋ค:
/**
* @memberof APIs.SeasonAPI
* @function CSeason API
* @description ํ๊ธฐ ์์ฑ API
* @version 2.0.0
*
* @param {Object} req
* @param {"POST"} req.method
* @param {"/seasons"} req.url
* @param {Object} req.user - "admin"|"manager"
* @param {Object} req.body
* @param {string} req.body.school - ObjectId of school
*
* @param {Object} res
* @param {Object} res.season - created season
*
* @throws {}
* | status | message | description |
* | :----- | :--------------- | :---------------------- |
* | 400 | SCHOOL_REQUIRED | school field is missing |
*/ํ๋ก์ ํธ ์ ์ฒด์์ Prettier๋ฅผ ์ฌ์ฉํ์ฌ ์ฝ๋ ํฌ๋งคํ ์ ํต์ผํฉ๋๋ค.
# ์ ์ฒด ์ฝ๋ ํฌ๋งคํ
yarn prettier
# ํน์ ํ์ผ ํฌ๋งคํ
npx prettier --write "src/components/MyComponent.tsx"| ์ค์ | ๊ฐ | ์ค๋ช |
|---|---|---|
printWidth |
80 | ํ ์ค ์ต๋ ๊ธธ์ด |
tabWidth |
2 | ํญ ํฌ๊ธฐ |
useTabs |
false | ์คํ์ด์ค ์ฌ์ฉ |
semi |
true | ์ธ๋ฏธ์ฝ๋ก ์ฌ์ฉ |
singleQuote |
false | ์๋ฐ์ดํ ์ฌ์ฉ |
trailingComma |
"es5" | ES5 ํธํ ํํ ์ผํ |
# ์ ์ฒด ํ์ผ ๋ฆฐํ
yarn lint
# ๋ณ๊ฒฝ๋ ํ์ผ๋ง ๋ฆฐํ
(Git diff ๊ธฐ๋ฐ)
yarn lincyarn linc๋ Git์์ ๋ณ๊ฒฝ๋ ํ์ผ๋ง ๊ฒ์ฌํ๋ฏ๋ก PR ์ ๋น ๋ฅธ ๊ฒ์ฆ์ ์ ์ฉํฉ๋๋ค.
ํ๋ก ํธ์๋๋ react-app ํ๋ฆฌ์
์ ๊ธฐ๋ฐ์ผ๋ก ํฉ๋๋ค:
{
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
}
}| ๊ท์น | ์ค๋ช |
|---|---|
no-unused-vars |
์ฌ์ฉํ์ง ์๋ ๋ณ์ ๊ธ์ง |
no-console |
console.log ์ ๊ฑฐ (ํ๋ก ํธ์๋) |
react-hooks/exhaustive-deps |
ํ ์์กด์ฑ ๋ฐฐ์ด ๊ฒ์ฌ |
react-hooks/rules-of-hooks |
ํ ์ฌ์ฉ ๊ท์น ๊ฒ์ฌ |
ํ๋ก์ ํธ๋ Airbnb JavaScript Style Guide๋ฅผ ์ฐธ๊ณ ํฉ๋๋ค. ์ฃผ์ ๊ท์น:
- const ์ฐ์ : ์ฌํ ๋น์ด ํ์ํ ๊ฒฝ์ฐ์๋ง
let์ฌ์ฉ,var๋ ์ฌ์ฉ ๊ธ์ง - ํ์ดํ ํจ์ ์ ํธ: ์ฝ๋ฐฑ ํจ์์ ํ์ดํ ํจ์ ์ฌ์ฉ
- ๋น๊ตฌ์กฐํ ํ ๋น: ๊ฐ์ฒด/๋ฐฐ์ด์์ ๊ฐ์ ์ถ์ถํ ๋ ์ฌ์ฉ
- ํ ํ๋ฆฟ ๋ฆฌํฐ๋ด: ๋ฌธ์์ด ์ฐ๊ฒฐ ๋์ ํ ํ๋ฆฟ ๋ฆฌํฐ๋ด ์ฌ์ฉ
- ๋ฐฐ์ด ๋ฉ์๋:
for๋ฃจํ ๋์map,filter,reduce์ฌ์ฉ
PR์ ์ฌ๋ฆฌ๊ธฐ ์ ์ ๋ค์ ํญ๋ชฉ์ ํ์ธํฉ๋๋ค:
-
yarn lint๋๋yarn linc๊ฐ ํต๊ณผํ๋๊ฐ? -
yarn prettier๋ก ํฌ๋งคํ ํ๋๊ฐ? - ํ๋์ฝ๋ฉ๋ ์์๊ฐ์ด ์๋๊ฐ? (CSS ๋ณ์ ์ฌ์ฉ)
- TypeScript ํ์ ์ด ์ ์๋์ด ์๋๊ฐ?
- JSDoc ์ฃผ์์ด ์์ฑ๋์ด ์๋๊ฐ? (๋ฐฑ์๋ ์ปจํธ๋กค๋ฌ)
- ์ด๋ฒคํธ ํธ๋ค๋ฌ์
handle์ ๋์ฌ๊ฐ ์๋๊ฐ? - ์ปดํฌ๋ํธ ์ด๋ฆ์ด PascalCase์ธ๊ฐ?
- ๋ถํ์ํ
console.log๊ฐ ๋จ์ ์์ง ์์๊ฐ?