Skip to content

Latest commit

ย 

History

History
530 lines (410 loc) ยท 12.1 KB

File metadata and controls

530 lines (410 loc) ยท 12.1 KB

์ฝ”๋”ฉ ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ

์ด ๋ฌธ์„œ์—์„œ๋Š” Altsis ํ”„๋กœ์ ํŠธ์—์„œ ๋”ฐ๋ฅด๋Š” ์ฝ”๋”ฉ ์Šคํƒ€์ผ๊ณผ ๊ทœ์น™์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์ผ๊ด€๋œ ์ฝ”๋“œ ์Šคํƒ€์ผ์€ ๊ฐ€๋…์„ฑ์„ ๋†’์ด๊ณ  ํ˜‘์—…์„ ์›ํ™œํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.


๋ชฉ์ฐจ

  1. ์ผ๋ฐ˜ ๊ทœ์น™
  2. ๋ช…๋ช… ๊ทœ์น™
  3. TypeScript ๊ทœ์น™
  4. React ์ปดํฌ๋„ŒํŠธ ๊ทœ์น™
  5. SCSS ๊ทœ์น™
  6. ๋ฐฑ์—”๋“œ JavaScript ๊ทœ์น™
  7. Prettier ์„ค์ •
  8. Linter ์„ค์ •
  9. ์ฐธ๊ณ  ์ž๋ฃŒ

1. ์ผ๋ฐ˜ ๊ทœ์น™

ํŒŒ์ผ ํ™•์žฅ์ž

ํŒŒ์ผ ์œ ํ˜• ํ™•์žฅ์ž ์‚ฌ์šฉ ์‹œ์ 
React ์ปดํฌ๋„ŒํŠธ .tsx JSX๋ฅผ ํฌํ•จํ•˜๋Š” ํŒŒ์ผ
TypeScript ๋ชจ๋“ˆ .ts JSX ์—†๋Š” ์œ ํ‹ธ/ํƒ€์ž…/ํ›…
์Šคํƒ€์ผ .module.scss ์ปดํฌ๋„ŒํŠธ ์Šค์ฝ”ํ”„ ์Šคํƒ€์ผ
์ „์—ญ ์Šคํƒ€์ผ .scss ์ „์—ญ ์Šคํƒ€์ผ (variables, global ๋“ฑ)
๋ฐฑ์—”๋“œ .js ES Module ๋ฌธ๋ฒ• ์‚ฌ์šฉ

ES6+ ๋ฌธ๋ฒ• ์‚ฌ์šฉ

// ํ™”์‚ดํ‘œ ํ•จ์ˆ˜ ์‚ฌ์šฉ (์ปดํฌ๋„ŒํŠธ, 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 ?? "์ด๋ฆ„ ์—†์Œ";

import ์ˆœ์„œ

// 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";

2. ๋ช…๋ช… ๊ทœ์น™

๋ณ€์ˆ˜/ํ•จ์ˆ˜

๋Œ€์ƒ ๊ทœ์น™ ์˜ˆ์‹œ
๋ณ€์ˆ˜ 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

3. TypeScript ๊ทœ์น™

์ธํ„ฐํŽ˜์ด์Šค์™€ ํƒ€์ž…

์ ‘๋‘์‚ฌ ์šฉ๋„ ์˜ˆ์‹œ
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>
  );
};

4. React ์ปดํฌ๋„ŒํŠธ ๊ทœ์น™

์ปดํฌ๋„ŒํŠธ ์„ ์–ธ

ํ™”์‚ดํ‘œ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:

// ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ์‹œ - ํ™”์‚ดํ‘œ ํ•จ์ˆ˜
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>;

5. SCSS ๊ทœ์น™

CSS ๋ณ€์ˆ˜ ์‚ฌ์šฉ ํ•„์ˆ˜

// ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ์‹œ
.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;
  }
}

6. ๋ฐฑ์—”๋“œ JavaScript ๊ทœ์น™

ES Module ์‚ฌ์šฉ

๋ฐฑ์—”๋“œ๋Š” "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 ์ฃผ์„

๋ชจ๋“  ์ปจํŠธ๋กค๋Ÿฌ ํ•จ์ˆ˜์— 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  |
 */

7. Prettier ์„ค์ •

ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ Prettier๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ”๋“œ ํฌ๋งคํŒ…์„ ํ†ต์ผํ•ฉ๋‹ˆ๋‹ค.

์‹คํ–‰ ๋ฐฉ๋ฒ•

# ์ „์ฒด ์ฝ”๋“œ ํฌ๋งคํŒ…
yarn prettier

# ํŠน์ • ํŒŒ์ผ ํฌ๋งคํŒ…
npx prettier --write "src/components/MyComponent.tsx"

์ฃผ์š” ์„ค์ •

์„ค์ • ๊ฐ’ ์„ค๋ช…
printWidth 80 ํ•œ ์ค„ ์ตœ๋Œ€ ๊ธธ์ด
tabWidth 2 ํƒญ ํฌ๊ธฐ
useTabs false ์ŠคํŽ˜์ด์Šค ์‚ฌ์šฉ
semi true ์„ธ๋ฏธ์ฝœ๋ก  ์‚ฌ์šฉ
singleQuote false ์Œ๋”ฐ์˜ดํ‘œ ์‚ฌ์šฉ
trailingComma "es5" ES5 ํ˜ธํ™˜ ํ›„ํ–‰ ์‰ผํ‘œ

8. Linter ์„ค์ •

ESLint ์‹คํ–‰

# ์ „์ฒด ํŒŒ์ผ ๋ฆฐํŒ…
yarn lint

# ๋ณ€๊ฒฝ๋œ ํŒŒ์ผ๋งŒ ๋ฆฐํŒ… (Git diff ๊ธฐ๋ฐ˜)
yarn linc

yarn linc๋Š” Git์—์„œ ๋ณ€๊ฒฝ๋œ ํŒŒ์ผ๋งŒ ๊ฒ€์‚ฌํ•˜๋ฏ€๋กœ PR ์ „ ๋น ๋ฅธ ๊ฒ€์ฆ์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

ESLint ์„ค์ •

ํ”„๋ก ํŠธ์—”๋“œ๋Š” 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 ํ›… ์‚ฌ์šฉ ๊ทœ์น™ ๊ฒ€์‚ฌ

9. ์ฐธ๊ณ  ์ž๋ฃŒ

Airbnb JavaScript Style Guide

ํ”„๋กœ์ ํŠธ๋Š” 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๊ฐ€ ๋‚จ์•„ ์žˆ์ง€ ์•Š์€๊ฐ€?

๋ชฉ์ฐจ๋กœ ๋Œ์•„๊ฐ€๊ธฐ