-
Notifications
You must be signed in to change notification settings - Fork 0
Frontend Coding Convention
์ฝ๋ฉ ์ปจ๋ฒค์ ์ด๋?
๊ฐ๋ฐ์๋ค๋ผ๋ฆฌ ์ฝ๊ณ , ๊ด๋ฆฌํ๊ธฐ ์ฌ์ด ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ํ ์ผ์ข ์ ์ฝ๋ฉ ์คํ์ผ ๊ท์ฝ์ด๋ค.
๋ชจ๋ ๊ฐ๋ฐ์๋ค์ ๊ฐ์ ์์ ๋ง์ ์คํ์ผ์ ๊ฐ์ง๊ณ ์์ง๋ง ์ด๋ ์ ๋์ ๊ฐ์ ์ฑ์ ๋ถ์ฌํ ์ฝ๋ฉ ์ปจ๋ฒค์ ์ ํตํด
์๋น์ค์ ํ์ง์ ์ํด ์ ๊ฒฝ์จ์ผ๋๋ค.
Next.js App Router
๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฌ์ฉ
_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
์๋ ํด๋ ๊ตฌ์กฐ์์ (list)
๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ฉด ๋ ์์์ ์๋ ํด๋์ ๋ฆฌ์คํธ ํ์ด์ง์์๋ง ์ฌ์ฉํ๋ ํด๋๊ฐ ๊ฒน์น๊ธฐ ๋๋ฌธ์ ์ด๋ฐ ์ํฉ์์๋ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ์ด์ ์ฌ์ฉํ๋ค.
โโโ _apis
โโโ _components # ๊ฒน์นจ
โโโ _types
โโโ (list)
โ โโโ _components # ๊ฒน์นจ
โ โโโ _hooks
โ โโโ page.tsx
โโโ [id]
โ โโโ _components
โ โโโ _hooks
โ โโโ page.tsx
- ๋ณ์๋ช
์
camelCase
๋ฅผ ์ฌ์ฉํ๋ค. - ๋ณต์ํ์ผ ๊ฒฝ์ฐ์๋ ๋ณต์์ ์๋ฏธ๋ฅผ ์ฌ์ฉํ๋ค.
- ์ํ์ ๋ํ ๋ณ์๋ฅผ ์ ์ธํ ๋
is
,has
๋ฑ์ ํ์ฉํ์.
// camelCase
const companyName = "openknowl";
// ๋จ์ & ๋ณต์
const tag = "๋ฏธ๋์ธํด";
const tags = ["์ฑ์ฉํ ๋ฏธ๋์ธํด", "๊ต์กํ ๋ฏธ๋์ธํด"];
// ์ํ ๋ณ์
const isMyCompany = false;
const hasNumber = false;
- ์ปดํฌ๋ํธ๋ช
์
PascalCase
๋ฅผ ์ฌ์ฉํ๋ค.
- ์ปดํฌ๋ํธ
props
์ ํ์ ์interface
๋ฅผ ์ฌ์ฉํ๊ณ์ปดํฌ๋ํธ๋ช
+Props
๋ฅผ ์ฌ์ฉํ๋ค.
interface CompanyProps {
name: string;
}
const Company: React.FC<CompanyProps> = ({ name }) => {
return (
<section>
<span>{name}</span>
</section>
);
};
export default Company;
- ํ์
์
PascalCase
๋ฅผ ์ฌ์ฉํ๋ค. - ์ ๋ฏธ์ฌ๋ก
Type
์ ์ฌ์ฉํ๋ค.
- ์ฌ์ฉ ์ ๋ฏธ์ฌ ๋ฆฌ์คํธ
-
*Type
: ์ผ๋ฐ์ ์ธ ํ์ ์ธ ๊ฒฝ์ฐ -
ArgsType
: ํจ์์arguments
ํ์ ์ธ ๊ฒฝ์ฐ -
ParamsType
:params
์ ํ์ ์ธ ๊ฒฝ์ฐ (/company/25
) -
QueriesType
:query
์ ํ์ ์ธ ๊ฒฝ์ฐ (?q=apple
) -
BodyType
:body
์ ํ์ ์ธ ๊ฒฝ์ฐ -
RequestType
:API
์์ฒญ ํ์ -
ResponseType
:API
์๋ต ํ์
-
- ํด๋์ค ๋ค์์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
kebab-case
๋ฅผ ์ฌ์ฉํ๋ค. - ์ปดํฌ๋ํธ๋ช ์ ์ ๋์ฌ๋ก ์ฌ์ฉํ๋ ๊ฒ์ ์์น์ผ๋ก ํ๋ค.
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;
- ์ต์์ ์๋ฆฌ๋จผํธ์
styled-components
๋ฅผ ์ฌ์ฉํ๊ณ ์ ๋ฏธ์ฌ๋กBlock
์ ๋ถ์ธ๋ค. - ์ต์์๋ฅผ ์ ์ธํ ์๋ฆฌ๋จผํธ๋
className
์ผ๋ก ์ ํํด์ ์คํ์ผ์ ๋ถ์ฌํ๋ค. (className
์ ์ปดํฌ๋ํธ๋ช ์ ๊ธฐ๋ฐ์ผ๋ก ์์ฑํ๋ค. ) -
className
์ ํด๋น ์ปดํฌ๋ํธ ์ด๋ฆ์kebab-case
์ ์ ๋์ฌ๋ก ์ฌ์ฉํ๋ค. - ์กฐ๊ฑด๋ถ ์คํ์ผ์ด ํ์ํ ๊ฒฝ์ฐ์๋ ์๋ก์ด
component
๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํ๋ค. -
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;
-
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;
}
๋ฐฐ์ด๊ณผ ๊ฐ์ฒด๋ฅผ ์ ์ธํ๋ ๊ฒฝ์ฐ์ ์์ฑ์ ํจ์๊ฐ ์๋ ๋ฆฌํฐ๋ด๋ก ์ ์ธํ๋ค.
// 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, };
- ํ์ดํํจ์๋ฅผ ์ต์ฐ์ ์ผ๋ก ์ฌ์ฉํ๋ค.
// BAD
console.log(typeof foo); // "function"
function foo() {
// ...
}
// GOOD
console.log(typeof foo); // Uncaught ReferenceError: foo is not defined
const foo = () => {
// ...
};
์ด๋ฒคํธ ํธ๋ค๋ฌ์ ์ ๋์ฌ๋ 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;
- ์ ๋ฏธ์ฌ๋ก
Api
๋ฅผ ์ฌ์ฉํ๋ค. -
get
๋ฉ์๋์ ๊ฒฝ์ฐApiKey
๋ฅผ ์ ์ํด์ ์ฌ์ฉํ๋ค. (swr
,axios
,fetch
์์ ์ฌ์ฉ๋๋end-point
๋ฅผ ๊ณต์ฉ์ผ๋ก ๊ด๋ฆฌํ๊ธฐ ์ํจ) -
RequestType
๊ณผResponseType
์ ์ ์ํด์ ์ฌ์ฉํ๋ค. -
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));
- ์๋ฌํธ๋ค๋ง์ ๋ง์ง๋ง ์ปดํฌ๋ํธ์์ ์ฒ๋ฆฌํ๋ ๊ฒ์ ์์น์ผ๋ก ํ๋ฉฐ, ์ด์ ๋จ๊ณ์์๋
throw error
๋ฅผ ์ฌ์ฉํด ์๋ฌ๋ฅผ ๋ฐํ์ํจ๋ค. - ์ฌ์ ์ ์ ์๋ ์๋ฌํธ๋ค๋ง ํจ์(
sendErrorToSentry
) ๋ฅผ ์ฌ์ฉํด ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ค. -
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>;
};
// .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',
},
},
],
};
// .prettierrc
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"arrowParens": "avoid"
}
์์ง ๋ฏธ์์ฑ์ด๊ณ ์ถ๊ฐ๋ก ์ ๋ฆฌํ ์์ ์ ๋๋ค :)