ESLint ์ค์ ์ค์ 'arrow-body-style': ['error', 'always']๋ผ๋ ๊ท์น์ด ์๋ค.
์ด ์ค์ ์ ์ผ๋ฉด ๋ชจ๋ ํ์ดํ ํจ์์์ ํญ์ ์ค๊ดํธ({})์ return ๋ฌธ์ ์ฌ์ฉํด์ผ ํ๋ค.
์๋์ฒ๋ผ ํ์ค๋ก ํํํ๊ธฐ ์ด๋ ค์ด ๊ฒฝ์ฐ์๋ return์ด ์์ด๋ ๋ณด๊ธฐ ์ข์ง๋ง,
setErrors((prev) => {
const errorMsg = validators[name] ? validators[name](value, values) : '';
return { ...prev, [name]: errorMsg };
});
์๋ ์ผ์ด์ค์ฒ๋ผ ํ ์ค๋ก ํํ์ด ์ถฉ๋ถํ ๊ฐ๋ฅํ ๊ฒฝ์ฐ์๋ return์ด ์ถ๊ฐ๋์ด ์์ฌ์ ๋ค.
const handlePasswordToggle = () => {
setShowPassword((prev) => {
return !prev;
});
};
๊ทผ๋ฐ ๋ ํ ์ค๋ก ํํ๊ฐ๋ฅํ๋๋ผ๋ JSX๋ฅผ ๋ฆฌํดํ๋ ๊ฒฝ์ฐ์๋ return ํค์๋๋ฅผ ๊ผญ ๋ฃ๊ณ ์ถ์๋ค.
const page = () => {
return <>ํ์ด์ง~</>
}
์ง๊ทนํ ๊ฐ์ธ ์ทจํฅ์ด์ง๋ง...๐ 'arrow-body-style' ์ค์ ๋ง์ผ๋ก๋
๋ด๊ฐ ์ํ๋ ๋์์ ์ธ๋ฐํ๊ฒ ์ปจํธ๋กคํ๊ธฐ ์ด๋ ค์ ๋ค.

๊ทธ๋์ ๋ฉํ ๋์ ์กฐ์ธ์ ํ์ ์ด ์ง์ ์ปค์คํ ESLint ํ๋ฌ๊ทธ์ธ์ ๋ง๋ค์ด๋ณด๊ธฐ๋ก ํ๋ค. (๋ ๋ค ๋์ ~~!)
์ด๋ฒ ๊ธ์์๋ ์ ์ ๊ณผ์ ๊ณผ ๊ฐ๋จํ ํ๊ธฐ๊น์ง ์ ๋ฆฌํด๋ณด๋ ค๊ณ ํ๋ค.
โ๏ธ ์ ์ ๊ณผ์
1๏ธโฃ module.exports
module.exports = {
meta: { ... },
create(context) { ... }
};
ESLint ๋ฃฐ์ ์ ์ํ ๋๋ ๊ฐ์ฒด๋ฅผ ๋ด๋ณด๋ด์ผ ํ๋ค.
์ด ๊ฐ์ฒด๋ ํฌ๊ฒ ๋ ๊ฐ์ง ํต์ฌ ์์ฑ์ผ๋ก ๊ตฌ์ฑ๋๋ค.
meta→ ๋ฃฐ์ ๋ฉํ์ ๋ณด (๋ฌธ์, ๋ฉ์์ง, ์์ ๊ฐ๋ฅ ์ฌ๋ถ ๋ฑ)create→ ์ค์ AST๋ฅผ ์ํํ๋ฉฐ ์ฝ๋๋ฅผ ๊ฒ์ฌํ๋ ํจ์
2๏ธโฃ meta ๊ฐ์ฒด
meta: {
type: "suggestion",
docs: {
description: "์ปดํฌ๋ํธ๋ ํญ์ ์ค๊ดํธ ์ฌ์ฉ, ์ผ๋ฐ ํจ์๋ ๊ฐ๊ฒฐํ๊ฒ ์์ฑ",
category: "Stylistic Issues",
recommended: false,
},
fixable: "code",
schema: [],
messages: {
unexpectedBraces:
"๊ฐ๋จํ ํ์ดํ ํจ์์๋ ์ค๊ดํธ์ return์ ์ฌ์ฉํ์ง ๋ง์ธ์",
expectedBraces: "์ปดํฌ๋ํธ ํจ์์๋ ์ค๊ดํธ์ return์ ์ฌ์ฉํ์ธ์",
},
},
type: "suggestion" → ์ฝ๋ ์คํ์ผ๊ณผ ๊ด๋ จ๋ ๊ถ์ฅ์ฌํญ์ ์ ์ํ๋ ๋ฃฐ์ด๋ค.
"problem": ์ฝ๋ ์ค๋ฅ/๋ฒ๊ทธ๋ฅผ ์ฐพ๋ ๋ฃฐ"suggestion": ์ฝ๋ ์คํ์ผ์ด๋ ๊ถ์ฅ ์ฌํญ"layout": ๋ค์ฌ์ฐ๊ธฐ, ๊ณต๋ฐฑ ๋ฑ ํฌ๋งท ๊ด๋ จ
docs → ESLint ๋ฌธ์์ ํ์๋ ์ค๋ช
๊ณผ ์นดํ
๊ณ ๋ฆฌ ์ ๋ณด์ด๋ค.
fixable: "code" → ESLint --fix ๋ช
๋ น์ผ๋ก ์๋ ์์ ๊ฐ๋ฅํจ์ ์๋ฏธํ๋ค.
"code": ์ฝ๋ ์์ ๊ฐ๋ฅ"whitespace": ๊ณต๋ฐฑ/ํฌ๋งท๋ง ์์ ๊ฐ๋ฅ
messages → ESLint๊ฐ ๊ฒฝ๊ณ /์๋ฌ๋ฅผ ํ์ํ ๋ ์ฌ์ฉํ ๋ฉ์์ง ๋ชฉ๋ก์ด๋ค.
3๏ธโฃ create(context) ํจ์
create ํจ์๋ ESLint๊ฐ AST๋ฅผ ์ํํ ๋ ํธ์ถ๋๋ ํต์ฌ ๋ถ๋ถ์ด๋ค.
context ๊ฐ์ฒด๋ฅผ ํตํด ์ฝ๋ ์์น ์ ๋ณด, ๊ฒฝ๊ณ ๋ณด๊ณ , ์๋ ์์ ๋ฑ์ ์ํํ ์ ์๋ค.
๋ฐํ๊ฐ์ผ๋ก๋ AST ๋ ธ๋ ํ์ ๋ณ ์ฝ๋ฐฑ ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค.
return {
ArrowFunctionExpression(node) {
...
}
};
์ด๋ ๊ฒ ํ๋ฉด ์ฝ๋์์ ํ์ดํ ํจ์๊ฐ ๋ฐ๊ฒฌ๋ ๋๋ง๋ค ํด๋น ์ฝ๋ฐฑ์ด ์คํ๋๋ค.
4๏ธโฃ isComponentFunction(node)
React ์ปดํฌ๋ํธ ํจ์๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํ ํฌํผ ํจ์์ด๋ค.
๋ ๊ฐ์ง ๊ธฐ์ค์ผ๋ก ํ๋ณํ๋ค.
โ 1. ํจ์ ์ด๋ฆ์ด ๋๋ฌธ์๋ก ์์ํ๋๊ฐ?
ํ์ดํ ํจ์๋ ์ต๋ช ํจ์์ด๊ธฐ ๋๋ฌธ์,
๋ถ๋ชจ ๋
ธ๋(VariableDeclarator)์ id.name์ ํตํด ์ด๋ฆ์ ํ์ธํด์ผ ํ๋ค.
→ ํ์ดํ ํจ์๋ ์ต๋ช ํจ์์ฌ์ id๊ฐ์ด null
const parent = node.parent;
if (parent.type === "VariableDeclarator" && parent.id && parent.id.name) {
if (/^[A-Z]/.test(parent.id.name)) {
return true;
}
}
// ํ์ดํ ํจ์๋ ์๋์ฒ๋ผ AST ํธ๋ฆฌ๊ฐ ๊ทธ๋ ค์ง
"declarations": [
{
"type": "VariableDeclarator", // ๋ณ์ ์ ์ธ ๋ถ๋ถ์ ํด๋นํจ
"start": 6,
"end": 53,
"id": {
"type": "Identifier",
"start": 6,
"end": 12,
"name": "Button"
},
...
]
โ 2. JSX๋ฅผ ๋ฐํํ๋๊ฐ?
ํจ์ ์ด๋ฆ์ด ํ์ค์นผ ์ผ์ด์ค๊ฐ ์๋๋๋ผ๋ JSX๋ฅผ ๋ฐํํ๋ฉด ์ปดํฌ๋ํธ๋ก ๊ฐ์ฃผํ๋ค.
if (node.body.type === "JSXElement" || node.body.type === "JSXFragment") {
return true;
}
// ํ์ดํ ํจ์ ์์
const Button1 = () => <>ํ์ด</>
const Button2 = () => <button>ํ์ด</button>
// AST ์์
{
"type": "ArrowFunctionExpression",
"id": null,
"body": {
"type": "JSXFragment", // <></>์ผ ๊ฒฝ์ฐ JSXFragment
}
}
๋ธ๋ก๋ฌธ ์์์ JSX๋ฅผ ๋ฆฌํดํ๋ ๊ฒฝ์ฐ๋ ์ฒ๋ฆฌํ๋ค.
if (node.body.type === "BlockStatement") {
const returnStatements = node.body.body.filter(
(stmt) => stmt.type === "ReturnStatement"
);
return returnStatements.some(
(stmt) =>
stmt.argument &&
(stmt.argument.type === "JSXElement" ||
stmt.argument.type === "JSXFragment")
);
}
// ํ์ดํ ํจ์ ์์
const List = ({loading}) => {
if (loading) {
return <div>๋ก๋ฉ์ค...</div>
}
return (
<ul>
<li>๋ฆฌ์คํธ</li>
</ul>
)
}
// AST ์์ (์ถ์ฝ)
"body": {
"type": "BlockStatement",
"start": 28,
"end": 137,
"body": [
"consequent": {
"type": "BlockStatement",
"start": 45,
"end": 79,
"body": [
{
"type": "ReturnStatement",
"argument": {
"type": "JSXElement",
},
{
"type": "ReturnStatement",
....
5๏ธโฃ isSingleReturn(node)
ํ ์ค๋ก ํํ ๊ฐ๋ฅํ ํ์ดํ ํจ์์ธ์ง ํ๋จํ๋ ํจ์์ด๋ค.
function isSingleReturn(node) {
// ์ฒซ ๋ฒ์งธ ์กฐ๊ฑด: body์ type์ด Block๋ฌธ์ด ์๋๋ฉด ์ด๋ฏธ ํ ์ค์ด๋ผ์ true๋ฅผ ๋ฆฌํด
if (node.body.type !== "BlockStatement") {
return true;
}
// block๋ฌธ์ด ์์๋ body์ length๊ฐ 1์ด ์๋๋ฉด ํ ์ค๋ก ํํํ์ง ๋ชปํ๋๋ก false๋ฅผ ๋ฆฌํด
const [statement] = node.body.body;
if (!statement || node.body.body.length !== 1) {
return false;
}
// block๋ฌธ์ด ์์ ๋ ๊ทธ ํ์
์ด ๋ฆฌํด์ด๊ณ argument๊ฐ ์๋ค๋ฉด ํ ์ค๋ก ํํ ๊ฐ๋ฅ
return statement.type === "ReturnStatement" && statement.argument;
}
6๏ธโฃ return {ArrowFunctionExpression}
ESLint๋ ๋ฐฉ๋ฌธ์(Visitor) ํจํด์ ์ฌ์ฉํด์ ๊ท์น์ ๊ฒ์ฌํ๋ค.
๊ทธ๋์ ArrowFunctionExpression๋ฅผ ์ฌ์ฉํด์ ํ์ดํ ํจ์๋ง ๊ฒ์ฌํ๋๋ก ๊ตฌํํ๋ค.
return {
ArrowFunctionExpression(node) {
...
};
์ด ๊ตฌ๋ฌธ์ fixํจ์๋ ์ถ๊ฐํด์ --fix ์ต์
์ผ๋ก ์๋ ์์ ๊ฐ๋ฅํ๋๋ก ๊ตฌํํ๋ค.
๐ NPM ๋ฐฐํฌ
npm publish
์์ฑํ ํ๋ฌ๊ทธ์ธ์ npm์ ๋ฐฐํฌํ๋ค.
๐ eslint-plugin-component-arrow-style
์ฒ์์ ๋ค ๋ง๋ค์๋ค๊ณ ์๊ฐํ๊ณ ๋ฌด์์ ๋ฐฐํฌ๋ฅผ ํด๋ฒ๋ ธ๋ค.
๊ทธ๋ฐ๋ฐ ์ฒซ ๋ฐฐํฌํ๊ณ ํ์ธํด๋ณด๋ README ๋ฌธ์๊ฐ ์์ด์ ๋ค์ ๋ฐฐํฌํ๋ค...
๋ฐฐํฌ๋ฅผ ํ๋ ค๋ฉด ๋งค๋ฒ ๋ฒ์ ์ ์ ๋ฐ์ดํธ๋ฅผ ํด์ค์ผ ํ๋ค๋ ์ฌ์ค์ ๋ชฐ๋ผ์
๋ฒ์ ์ ๋ฐ์ดํธ ๋ช ๋ น์ด๋ฅผ ์ค์๋ก ์ ๋ ฅํ๋ค๊ฐ 1.0.1 ๋ฒ์ ์ ๋ ๋ฆฌ๊ณ …๐ซ
์ด์ฐ์ ์ฐ ๋ฆฌ๋๋ฏธ ๋ฌธ์๋ฅผ ์ถ๊ฐํ 1.0.2 ๋ฒ์ ์ ์ ๋ฐ์ดํธ ํ๋ค.
๐ ๋ฒ๊ทธ ๋ฐ๊ฒฌ
๋ช ๊ฐ์ง ํ ์คํธ ์ผ์ด์ค๋ฅผ ๋ง๋ค์ด์ ๋ฐฐํฌ ์ ์ ํ ์คํธ๋ฅผ ํด๋ดค์ง๋ง,
์ค์ ์ฌ์ฉํด๋ณด๋ ๋ด๊ฐ ๊ณ ๋ คํ์ง ๋ชป ํ๋ ์ผ์ด์ค๊ฐ ์์๋ค….๐
์ง๊ธ๊น์ง ๋ฐ๊ฒฌํ ๋ฒ๊ทธ๋ ๋ ๊ฐ์ง์ด๋ค.
1๏ธโฃ ๊ฐ์ฒด ๋ฆฌํด ์ ๊ดํธ ๋๋ฝ ๋ฌธ์
// ์๋ ์ฝ๋ -> ์ด๋ ๊ฒ ์์ ๋์ด์ผ ํ๋๋ฐ...
setValues((prev) => ({ ...prev, [name]: value }));
// ์๋ชป ์์ ๋ ๊ฒฐ๊ณผ ๐คฏ
setValues((prev) => { ...prev, [name]: value });
fix ํจ์์์ ๊ฐ์ฒด ๋ฆฌํด์ ๊ณ ๋ คํ์ง ๋ชป ํ๊ณ ,
์ด๋ก ์ธํด --fix๋ฅผ ์คํํ๋ฉด ์ฝ๋๊ฐ ์๋ชป ์์ ๋์ด ์๋ฌ๊ฐ ๋ฐ์ํ๋ ๋ฌธ์ ๊ฐ ์๊ฒผ๋ค. ๐คฏ
2๏ธโฃ map()์์ JSX ๋ฆฌํด ์ ์ฝ๋ ๊นจ์ง
map() ๋ด๋ถ์์ ์ปดํฌ๋ํธ๋ฅผ ๋ฆฌํดํ ๋,
fix๊ฐ ์ ์ฉ๋๋ฉด ์ฝ๋๊ฐ ๋น์ ์์ ์ผ๋ก ์์ ๋๋ ๋ฌธ์ ๊ฐ ์๋ค.
์ด ๋ถ๋ถ๋ 1๋ฒ๊ณผ ๋์ผํ๊ฒ fix ๋ก์ง์ ์์ ํด์ผ ํ ๊ฒ ๊ฐ๋ค.
๋ ๋ง์ ํ๋ฌ๊ทธ์ธ์ ์ง์ ์ฌ์ฉํด๋ณด๋,
map() ๋ด๋ถ์์๋ return์ ์ ๊ฑฐํ๋๊ฒ ์ข ๋ ๊น๋ํด๋ณด์ฌ์
์ด ๋ถ๋ถ์ ์ถ๊ฐํ ์ง ๊ณ ๋ฏผ ์ค์ด๋ค.
๐ญ ๋ง๋ฌด๋ฆฌ
์ฒ์์๋ ์ด๋ป๊ฒ ์์ํด์ผ ํ ์ง ๋ชฐ๋ผ์ ๊ฐ์ ์ก๊ธฐ ์ด๋ ค์ ์ง๋ง,
์ฐจ๊ทผ์ฐจ๊ทผ ์ฐพ์๋ณด๋ฉฐ ์ง์ ๋ง๋ค์ด๋ณด๋ ์๊ฐ๋ณด๋ค ์ฌ๋ฐ์๋ค.
ํนํ JS ์ฝ๋๊ฐ AST๋ก ๋ณํ๋๋ค๋ ๋ง์ ๋ง์ด ๋ค์ด์๋๋ฐ,
์ด๋ฒ์ AST ๊ตฌ์กฐ๋ฅผ ์ ์ฉํด์ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๋ ์ข ๋ ์ดํดํ๊ธฐ ์์ํด์ง ๊ฒ ๊ฐ๋ค.
๋, ์ด๋ฒ ๊ฒฝํ์ ํตํด ํ ์ฝ๋ ์คํ์ผ์ ESLint๋ก ๊ด๋ฆฌํ๋ฉด ํจ์ฌ ํจ์จ์ ์ด๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
๋งค๋ฒ ๊ท์น์ ๊ธฐ์ตํ๊ธฐ๋ณด๋ค๋ --fix๋ ์ ์ฅ์ผ๋ก ํ ๋ฒ์ ์ ๋ฆฌ๋๋ค๋ฉด ํธํ ๊ฒ ๊ฐ๋ค.
์๋ฌดํผ ์ด๋ฒ ๊ธฐํ์ ๋ง๋ค์ด๋ณผ ์ ์์ด์ ์ฌ๋ฐ์๋ค. ๐บ
๐ ์ฐธ๊ณ
https://eslint.org/docs/latest/extend/custom-rules
Custom Rules - ESLint - Pluggable JavaScript Linter
A pluggable and configurable linter tool for identifying and reporting on patterns in JavaScript. Maintain your code quality with ease.
eslint.org
AST explorer
astexplorer.net
https://blog.eunsukim.me/posts/about-javascript-abstract-syntax-tree-and-eslint-rules
TIL - AST(Abstract Syntax Tree)์ ESLint Rules
Today I learned - Abstract Syntax Tree์ ESLint Rules์ ๊ดํ์ฌ
blog.eunsukim.me
https://fantasmith.com/challenge/opensource/fsd_lint/
FSD๋ฅผ ์ํ ESLint ํ๋ฌ๊ทธ์ธ ๊ฐ๋ฐ ์ผ์ง | Zen's Atelier
FSD ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์, ์๋์ผ๋ก ์ค์ ํ๋ ๋ฐ ๋ถํธํจ์ ๋๊ผ๋ค. ์ ๋๋ก๋ ํ๋ฌ๊ทธ์ธ์ด ์กด์ฌํ์ง ์์๊ณ , ๊ฐ๋ฐ์ ํธํ๊ฒ ํ๊ธฐ ์ํด ESLint ๋ชจ๋์ ๋ง๋ค์ด๋ณด๊ธฐ๋ก ํ๋ค.
fantasmith.com