Skip to content

Frontend Coding Convention

๋ฐ•์ƒ์€ edited this page Mar 16, 2025 · 1 revision

์ฝ”๋”ฉ ์ปจ๋ฒค์…˜์ด๋ž€?

๊ฐœ๋ฐœ์ž๋“ค๋ผ๋ฆฌ ์ฝ๊ณ , ๊ด€๋ฆฌํ•˜๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์ผ์ข…์˜ ์ฝ”๋”ฉ ์Šคํƒ€์ผ ๊ทœ์•ฝ์ด๋‹ค.

๋ชจ๋“  ๊ฐœ๋ฐœ์ž๋“ค์€ ๊ฐ์ž ์ž์‹ ๋งŒ์˜ ์Šคํƒ€์ผ์„ ๊ฐ€์ง€๊ณ  ์žˆ์ง€๋งŒ ์–ด๋А ์ •๋„์˜ ๊ฐ•์ œ์„ฑ์„ ๋ถ€์—ฌํ•œ ์ฝ”๋”ฉ ์ปจ๋ฒค์…˜์„ ํ†ตํ•ด

์„œ๋น„์Šค์˜ ํ’ˆ์งˆ์„ ์œ„ํ•ด ์‹ ๊ฒฝ์จ์•ผ๋œ๋‹ค.

๐Ÿ•น๏ธ ํด๋” ๋ฐ ํŒŒ์ผ ๊ตฌ์กฐ

Next.js App Router๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉ

0๏ธโƒฃ ์‚ฌ์ „ ์ง€์‹

  1. Private Folder
  2. Route Group

1๏ธโƒฃ ๊ณต์šฉ ํŒŒ์ผ์— ๋Œ€ํ•œ ๊ตฌ์กฐ

_apis, _components, _hooks, _types ๋“ฑ ํด๋”๋ช…์€ ๋ชจ๋‘ _๋กœ ์‹œ์ž‘ํ•˜๊ณ , ๋ณต์ˆ˜ํ˜•์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค. component, type, hook, api ๋“ฑ ๊ฐ ํŽ˜์ด์ง€์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํŒŒ์ผ์€ Private Folder๋ฅผ ์ด์šฉํ•ด์„œ ๋งŒ๋“ ๋‹ค.
๋งŒ์•ฝ ๋‘ ๊ฐœ ์ด์ƒ์˜ ํŽ˜์ด์ง€์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํŒŒ์ผ์ด๋ผ๋ฉด ๋‘๊ฐœ์˜ ํŽ˜์ด์ง€์˜ ๊ณตํ†ต๋œ ๋ถ€๋ชจ๋กœ ๋Œ์–ด์˜ฌ๋ ค์„œ ์‚ฌ์šฉํ•œ๋‹ค.
(2025.01.06)๋‘ ๊ฐœ ์ด์ƒ์˜ ํŽ˜์ด์ง€์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํŒŒ์ผ์ด๋ผ๋ฉด ๋จผ์ € ์„ ์–ธํ•œ ๊ณณ์—์„œ import ํ•˜๋Š” ๊ฒƒ์„ ์›์น™์œผ๋กœ ํ•œ๋‹ค.

โ”œโ”€โ”€ _apis
โ”‚   โ””โ”€โ”€ index.ts
โ”œโ”€โ”€ _components
โ”‚   โ”œโ”€โ”€ HappyfolioCard.tsx
โ”‚   โ”œโ”€โ”€ HappyfolioDetailMainInfoTitle.tsx
โ”‚   โ”œโ”€โ”€ HappyfolioList.tsx
โ”‚   โ”œโ”€โ”€ HappyfolioLoadingCard.tsx
โ”‚   โ””โ”€โ”€ HappyfolioLoadingCardList.tsx
โ”œโ”€โ”€ _types
โ”‚   โ””โ”€โ”€ index.ts
โ”œโ”€โ”€ (list)
โ”‚   โ”œโ”€โ”€ _components
โ”‚   โ”œโ”€โ”€ _hooks
โ”‚   โ””โ”€โ”€ page.tsx
โ”œโ”€โ”€ [id]
โ”‚   โ”œโ”€โ”€ _components
โ”‚   โ”œโ”€โ”€ _hooks
โ”‚   โ””โ”€โ”€ page.tsx
โ””โ”€โ”€ layout.tsx

2๏ธโƒฃ ๊ทธ๋ฃน ํด๋” ์‚ฌ์šฉ ์˜ˆ์‹œ

์•„๋ž˜ ํด๋” ๊ตฌ์กฐ์—์„œ (list)๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด ๋” ์ƒ์œ„์— ์žˆ๋Š” ํด๋”์™€ ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ํด๋”๊ฐ€ ๊ฒน์น˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ๋Š” ๊ทธ๋ฃน์œผ๋กœ ๋ฌถ์–ด์„œ ์‚ฌ์šฉํ•œ๋‹ค.

โ”œโ”€โ”€ _apis
โ”œโ”€โ”€ _components # ๊ฒน์นจ
โ”œโ”€โ”€ _types
โ”œโ”€โ”€ (list)
โ”‚   โ”œโ”€โ”€ _components # ๊ฒน์นจ
โ”‚   โ”œโ”€โ”€ _hooks
โ”‚   โ””โ”€โ”€ page.tsx
โ”œโ”€โ”€ [id]
โ”‚   โ”œโ”€โ”€ _components
โ”‚   โ”œโ”€โ”€ _hooks
โ”‚   โ””โ”€โ”€ page.tsx

๐Ÿ“– ๋„ค์ด๋ฐ ๊ทœ์น™

0๏ธโƒฃ ๋ณ€์ˆ˜

  1. ๋ณ€์ˆ˜๋ช…์€ camelCase๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  2. ๋ณต์ˆ˜ํ˜•์ผ ๊ฒฝ์šฐ์—๋Š” ๋ณต์ˆ˜์˜ ์˜๋ฏธ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  3. ์ƒํƒœ์— ๋Œ€ํ•œ ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธํ• ๋•Œ is, has ๋“ฑ์„ ํ™œ์šฉํ•˜์ž.
// camelCase
const companyName = "openknowl";
// ๋‹จ์ˆ˜ & ๋ณต์ˆ˜
const tag = "๋ฏธ๋‹ˆ์ธํ„ด";
const tags = ["์ฑ„์šฉํ˜• ๋ฏธ๋‹ˆ์ธํ„ด", "๊ต์œกํ˜• ๋ฏธ๋‹ˆ์ธํ„ด"];
// ์ƒํƒœ ๋ณ€์ˆ˜
const isMyCompany = false;
const hasNumber = false;

1๏ธโƒฃ ์ปดํฌ๋„ŒํŠธ

  1. ์ปดํฌ๋„ŒํŠธ๋ช…์€ PascalCase๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  2. ์ปดํฌ๋„ŒํŠธ props์˜ ํƒ€์ž…์€ interface๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์ปดํฌ๋„ŒํŠธ๋ช… + Props๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
interface CompanyProps {
  name: string;
}
const Company: React.FC<CompanyProps> = ({ name }) => {
  return (
    <section>
      <span>{name}</span>
    </section>
  );
};
export default Company;

2๏ธโƒฃ ํƒ€์ž…

  1. ํƒ€์ž…์€ PascalCase๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  2. ์ ‘๋ฏธ์‚ฌ๋กœ Type์„ ์‚ฌ์šฉํ•œ๋‹ค.
  • ์‚ฌ์šฉ ์ ‘๋ฏธ์‚ฌ ๋ฆฌ์ŠคํŠธ
    1. *Type: ์ผ๋ฐ˜์ ์ธ ํƒ€์ž…์ธ ๊ฒฝ์šฐ
    2. ArgsType: ํ•จ์ˆ˜์˜ arguments ํƒ€์ž…์ธ ๊ฒฝ์šฐ
    3. ParamsType: params์˜ ํƒ€์ž…์ธ ๊ฒฝ์šฐ ( /company/25 )
    4. QueriesType: query์˜ ํƒ€์ž…์ธ ๊ฒฝ์šฐ ( ?q=apple )
    5. BodyType: body์˜ ํƒ€์ž…์ธ ๊ฒฝ์šฐ
    6. RequestType: API ์š”์ฒญ ํƒ€์ž…
    7. ResponseType: API ์‘๋‹ต ํƒ€์ž…

3๏ธโƒฃ className

  1. ํด๋ž˜์Šค ๋„ค์ž„์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ kebab-case๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  2. ์ปดํฌ๋„ŒํŠธ๋ช…์„ ์ ‘๋‘์‚ฌ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ์›์น™์œผ๋กœ ํ•œ๋‹ค.
import styled from 'styled-components';
const CompanyBlock = styled.section`
  .company-name {
    font-size: 20px;
    font-weight: bold;
  }
`;
interface CompanyProps {
  name: string;
}
const Company: React.FC<CompanyProps> = ({ name }) => {
  return (
    <ChampionBlock>
      <span className="company-name">{name}</span>
    </ChampionBlock>
  );
};
export default Company;

4๏ธโƒฃ styled-components

  1. ์ตœ์ƒ์œ„ ์—˜๋ฆฌ๋จผํŠธ์— styled-components๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์ ‘๋ฏธ์‚ฌ๋กœ Block์„ ๋ถ™์ธ๋‹ค.
  2. ์ตœ์ƒ์œ„๋ฅผ ์ œ์™ธํ•œ ์—˜๋ฆฌ๋จผํŠธ๋Š” className์œผ๋กœ ์„ ํƒํ•ด์„œ ์Šคํƒ€์ผ์„ ๋ถ€์—ฌํ•œ๋‹ค. ( className์€ ์ปดํฌ๋„ŒํŠธ๋ช…์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘์„ฑํ•œ๋‹ค. )
  3. className์€ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ ์ด๋ฆ„์„ kebab-case์˜ ์ ‘๋‘์‚ฌ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.
  4. ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋Š” ์ƒˆ๋กœ์šด component๋ฅผ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•œ๋‹ค.
  5. className์— ์กฐ๊ฑด๋ฌธ์„ ๋„ฃ๋Š”๊ฒƒ์„ ์ง€์–‘ํ•œ๋‹ค.
import styled from 'styled-components';
const CompanyBlock = styled.section`
  // ...
`;
const CompanyInfo = styled.div<{hasMoney: boolean}>`
   // ...
`
const Company: React.FC<CompanyProps> = () => {
  return (
    <CompanyBlock>
      <span className="company-name">์˜คํ”ˆ๋†€</span>
      <CompanyInfo hasMoney />
    </CompanyBlock>
  );
};
export default Company;

๐Ÿ““ ์ฝ”๋“œ

0๏ธโƒฃ if๋ฌธ

  1. if ~ else๋‚˜ ์ค‘์ฒฉ if๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋ณด๋‹ค๋Š” ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด early return์„ ์‚ฌ์šฉํ•˜์ž.
type RoundCheckboxColorType = 'blue' | 'navy' | 'black';
// BAD
const getFontColor = (checked: boolean, color: RoundCheckboxColorType) => {
  if (checked) {
    if (color === 'blue') {
      return palette.blue_500;
    }
    if (color === 'black') {
      return palette.black;
    }
  }
  return palette.black;
};
// GOOD
const getFontColor = (checked: boolean, color: RoundCheckboxColorType) => {
  if (!checked) return palette.black;
  if (color === 'blue') return palette.blue_500;
  if (color === 'black') return palette.black;
  
  return palette.black;
};

ํ•œ์ค„์งœ๋ฆฌ ๊ฐ„๋‹จํ•œ ์กฐ๊ฑด๋ฌธ์ด๋ผ๋ฉด ๋ธ”๋Ÿญ({})์œผ๋กœ ๊ฐ์‹ธ์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ๋ฆฌํ„ดํ•ด๋„ ๋˜์ง€๋งŒ, ๋‘ ์ค„ ์ด์ƒ์ด๋ฉด ๋ธ”๋Ÿญ์œผ๋กœ ๊ฐ์‹ธ์„œ ๋ฆฌํ„ดํ•˜์ž.

// BAD
if (videoRef.current && videoRef.current.clientHeight > 0) return setSpeedControllerMaxHeight(videoRef.current.clientHeight - 60);
// GOOD
if (videoRef.current && videoRef.current.clientHeight > 0) {
  return setSpeedControllerMaxHeight(videoRef.current.clientHeight - 60);
}
// GOOD
if (isFirst) return;
if (isFirst) {
  return;
}

1๏ธโƒฃ ๋ฐฐ์—ด๊ณผ ๊ฐ์ฒด ์„ ์–ธ

๋ฐฐ์—ด๊ณผ ๊ฐ์ฒด๋ฅผ ์„ ์–ธํ•˜๋Š” ๊ฒฝ์šฐ์—” ์ƒ์„ฑ์ž ํ•จ์ˆ˜๊ฐ€ ์•„๋‹Œ ๋ฆฌํ„ฐ๋Ÿด๋กœ ์„ ์–ธํ•œ๋‹ค.

// BAD
const emptyArr = new Array(); // []
const arr = new Array(1, 2, 3, 4); // [1, 2, 3, 4]
const emptyObj = new Object(); // {}
const obj = new Object();
obj.first = 1;
console.log(obj); // {first: 1};
// GOOD
const emptyArr = [];
const arr = [1, 2, 3, 4];
const emptyObj = {};
const obj = { first: 1, };

2๏ธโƒฃ ํ•จ์ˆ˜

  1. ํ™”์‚ดํ‘œํ•จ์ˆ˜๋ฅผ ์ตœ์šฐ์„ ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.
// BAD
console.log(typeof foo); // "function"
function foo() {
  // ...
}
// GOOD
console.log(typeof foo); // Uncaught ReferenceError: foo is not defined
const foo = () => {
  // ...
};

3๏ธโƒฃ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์˜ ์ ‘๋‘์‚ฌ๋Š” on์œผ๋กœ ์‚ฌ์šฉํ•˜๊ณ , ๊ตฌ์ฒด์ ์ธ ํ–‰์œ„์— ๋Œ€ํ•œ ์ด๋ฆ„์„ ๋ถ™์—ฌ์ค€๋‹ค.

  • ex) ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํ”„๋กœ์ ํŠธ๊ฐ€ ์ˆ˜์ •๋˜๋Š” ํ•ธ๋“ค๋Ÿฌ์ธ ๊ฒฝ์šฐ onClick + UpdateProject ( ๊ตฌ์ฒด์ ์ธ ํ–‰์œ„ ) + Button ๋‹จ, ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜๋‚ด์—์„œ ๋ชจ๋“  ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ณ  ํ•˜๋‚˜์˜ ํ–‰์œ„๋Š” ํ•˜๋‚˜์˜ ํ•จ์ˆ˜๋กœ ๊ตฌ๋ถ„ํ•ด์„œ ์ž‘์„ฑํ•œ๋‹ค.
  • ex) updateProject(), openToast(), loadingSpinner(), closeSpinner()
const Openknowl: React.FC = () => {
  const updateProject = async () => {
    // ...
  };
  const openToast = () => {
    // ...
  };
  const onClickUpdateProjectButton = async () => {
    try {
      loadingSpinner();
      await updateProject();
    } catch (error) {
      sendErrorToSentry(error);
      openToast();
    } 
  };
  return (
      <button onClick={onClickUpdateProjectButton}>update project</button>
  );
};
export default Openknowl;

๋‹จ, ํ•œ์ค„์งœ๋ฆฌ ๊ฐ„๋‹จํ•œ ํ•ธ๋“ค๋Ÿฌ๋ผ๋ฉด ์ธ๋ผ์ธ์œผ๋กœ ์ž‘์„ฑํ•œ๋‹ค.

import { useState } from 'react';
import styled from 'styled-components';
const StyledOpenknowlBlock = styled.section``;
const Openknowl: React.FC = () => {
  const [toggle, setToggle] = useState(false);
  return (
    <StyledOpenknowlBlock>
      <button onClick={() => setToggle(prev => !prev)}>click me : {toggle}</button>
    </StyledOpenknowlBlock>
  );
};
export default Openknowl;

4๏ธโƒฃ Api ํ•จ์ˆ˜

  1. ์ ‘๋ฏธ์‚ฌ๋กœ Api๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  2. get ๋ฉ”์†Œ๋“œ์˜ ๊ฒฝ์šฐ ApiKey๋ฅผ ์ •์˜ํ•ด์„œ ์‚ฌ์šฉํ•œ๋‹ค. (swr, axios, fetch ์—์„œ ์‚ฌ์šฉ๋˜๋Š” end-point๋ฅผ ๊ณต์šฉ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•จ)
  3. RequestType ๊ณผ ResponseType์„ ์ •์˜ํ•ด์„œ ์‚ฌ์šฉํ•œ๋‹ค.
  4. fetchํ•จ์ˆ˜์˜ ๊ฒฝ์šฐ ์‚ฌ์ „์— ์ •์˜๋œ fetchWrapper๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
export const getEventsApiKey = (queries: EventQueries) => makeUrlQueries('/api/v3/events', queries);
export const getEventsApi = (queries: EventQueries) =>
  axios.get<EventResponseType>(getEventsApiKey(queries));
export const fetchEventsApi = async (queries: EventQueries) =>
  await fetchWrapper<EventResponseType>(getEventsApiKey(queries));

4๏ธโƒฃ ์—๋Ÿฌํ•ธ๋“ค๋ง (try - catch)

  1. ์—๋Ÿฌํ•ธ๋“ค๋ง์€ ๋งˆ์ง€๋ง‰ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ์›์น™์œผ๋กœ ํ•˜๋ฉฐ, ์ด์ „ ๋‹จ๊ณ„์—์„œ๋Š” throw error๋ฅผ ์‚ฌ์šฉํ•ด ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜์‹œํ‚จ๋‹ค.
  2. ์‚ฌ์ „์— ์ •์˜๋œ ์—๋Ÿฌํ•ธ๋“ค๋ง ํ•จ์ˆ˜(sendErrorToSentry) ๋ฅผ ์‚ฌ์šฉํ•ด ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค.
  3. catch๋ฌธ์— error์ธ์ž๋Š” "error" ๋กœ ํ†ต์ผํ•œ๋‹ค.
// useParticipants.tsx (hook)
const updateParticipants = async () => {
  try {
    const response = await updateEventParticipantsAPI(id, participations);
    return response.data;
  } catch (error) {
    throw error;
  }
};
// Participant.tsx (view)
const Participant: React.FC = () => {
  const onClickButton = () => {
    try {
      updateParticipants();
    } catch (error) {
      sendErrorToSentry(error);
    }
  };
  return <button onClick={onClickButton}>ํด๋ฆญ</button>;
};

๐Ÿช„ Prettier & Eslint

0๏ธโƒฃ Eslint

// .eslintrc.cjs
module.exports = {
  extends: ['turbo', 'next/core-web-vitals', 'plugin:prettier/recommended'],
  plugins: ['unused-imports'], //* ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” import ์ฒดํฌ,
  settings: {
    react: {
      version: 'detect',
    },
  },
  rules: {
    'react/no-unescaped-entities': 'off',
    '@next/next/no-img-element': 'off' /* <img>ํƒœ๊ทธ ์‚ฌ์šฉ ๊ฐ€๋Šฅ*/,
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'off' /* react hook dependencies ์ž์œจ์„ฑ์„ ์คŒ*/,
    '@next/next/no-html-link-for-pages': 'off' /* <a>ํƒœ๊ทธ ์‚ฌ์šฉ๊ฐ€๋Šฅ*/,
    'unused-imports/no-unused-imports': 'error',
    'react/display-name': 'off',
    'no-empty-function': 'off',
  },
  overrides: [
    {
      files: ['**/*.ts', '**/*.tsx'],
      plugins: ['@typescript-eslint'],
      extends: ['plugin:@typescript-eslint/recommended'],
      parser: '@typescript-eslint/parser',
      rules: {
        /*
        const _a = 'unused, with underscore, no warning'
        const b = 'unused, no underscore, warning'
        */
        '@typescript-eslint/no-explicit-any': 'off',
        'no-unused-vars': 'off',
        '@typescript-eslint/no-empty-function': ['off'],
        '@typescript-eslint/no-non-null-assertion': 'off',
        '@typescript-eslint/no-unused-vars': [
          'error',
          {
            argsIgnorePattern: '^_',
            varsIgnorePattern: '^_',
            caughtErrorsIgnorePattern: '^_',
            ignoreRestSiblings: true,
          },
        ],
        '@typescript-eslint/no-empty-interface': 'off',
        /** RequestType<Params = {}, Queries = {}, Body = {}>์—์„œ error ๋ฐœ์ƒ */
        '@typescript-eslint/ban-types': 'off',
      },
    },
  ],
};

1๏ธโƒฃ Prettier

// .prettierrc
{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100,
  "arrowParens": "avoid"
}

์•„์ง ๋ฏธ์™„์„ฑ์ด๊ณ  ์ถ”๊ฐ€๋กœ ์ •๋ฆฌํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค :)