๐ ๏ธ Codemod ์ค์ ๊ฐ์ด๋ - jscodeshift๋ก ์๋ฐฑ ๊ฐ ํ์ผ ๋ฆฌํฉํ ๋ง ์๋ํํ๊ธฐ
โข2026๋ 2์ 12์ผ ๋ชฉ์์ผ
Codemod๊ฐ ํ์ํ ์๊ฐ
ํ๋ก์ ํธ์์ deprecated๋ API๋ฅผ ์ API๋ก ๋ฐ๊ฟ์ผ ํ๋ ์ํฉ์ ๊ฒช์ด๋ณธ ์ ์๋์? ํ์ผ์ด 10๊ฐ๋ผ๋ฉด ์์์ ์ผ๋ก ํ ์ ์์ด์. ํ์ง๋ง 100๊ฐ, 1,000๊ฐ ํ์ผ์ด๋ผ๋ฉด ์ด์ผ๊ธฐ๊ฐ ๋ฌ๋ผ์ ธ์.
์ด ๊ธ์์๋ jscodeshift๋ฅผ ์ฌ์ฉํด codemod๋ฅผ ์์ฑํ๊ณ , ๋๊ท๋ชจ ์ฝ๋๋ฒ ์ด์ค๋ฅผ ์์ ํ๊ฒ ๋ฆฌํฉํ ๋งํ๋ ๋ฐฉ๋ฒ์ ๋จ๊ณ๋ณ๋ก ๋ค๋ค์. AST ๊ธฐ์ด๋ถํฐ ์ค์ API ๋ง์ด๊ทธ๋ ์ด์ , ํ ์คํธ๊น์ง ๋ค๋ฃจ๊ณ ์์ด์.
์ด ๊ธ์ JavaScript/TypeScript ๊ธฐ๋ณธ ๋ฌธ๋ฒ์ ์ดํดํ๊ณ , ํ๋ก์ ํธ์์ ๋ฐ๋ณต์ ์ธ ์ฝ๋ ๋ณ๊ฒฝ ์์ ์ ์๋ํํ๊ณ ์ถ์ ๊ฐ๋ฐ์๋ฅผ ๋์์ผ๋ก ํด์.
์ ๊ท์์ ํ๊ณ
๊ฐ๋จํ ์นํ์ sed๋ IDE์ ์ฐพ๊ธฐ-๋ฐ๊พธ๊ธฐ๋ก ์ถฉ๋ถํด์. ํ์ง๋ง ๋ค์๊ณผ ๊ฐ์ ๊ฒฝ์ฐ์๋ ์ ๊ท์๋ง์ผ๋ก ํด๊ฒฐํ๊ธฐ ์ด๋ ค์์.
- ํจ์ ํธ์ถ์ ์ธ์ ์์๋ฅผ ๋ฐ๊ฟ์ผ ํ ๋
- ํน์ ์กฐ๊ฑด์์๋ง import๋ฅผ ๋ณ๊ฒฝํด์ผ ํ ๋
- ๋ณ์ ์ค์ฝํ๋ฅผ ๊ณ ๋ คํ ์ด๋ฆ ๋ณ๊ฒฝ์ด ํ์ํ ๋
- JSX ์์ฑ์ ์ถ๊ฐํ๊ฑฐ๋ ์ ๊ฑฐํด์ผ ํ ๋
์ด๋ฐ ์์ ์๋ ์ฝ๋์ ๊ตฌ์กฐ์ ์๋ฏธ๋ฅผ ์ดํดํ๋ ๋๊ตฌ๊ฐ ํ์ํด์. ๊ทธ๊ฒ ๋ฐ๋ก codemod์์.
AST์ jscodeshift ๊ธฐ์ด
AST๋
AST(Abstract Syntax Tree)๋ ์ฝ๋๋ฅผ ํธ๋ฆฌ ๊ตฌ์กฐ๋ก ํํํ ๊ฒ์ด์์. ๋ค์ ์ฝ๋๋ฅผ ์๋ก ๋ค์ด๋ณผ๊ฒ์.
const greeting = "hello";์ด ํ ์ค์ AST์์ VariableDeclaration โ VariableDeclarator โ StringLiteral ๊ฐ์ ๋
ธ๋ ํธ๋ฆฌ๋ก ๋ณํ๋ผ์. codemod๋ ์ด ํธ๋ฆฌ๋ฅผ ํ์ํ๊ณ ์์ ํ ๋ค ๋ค์ ์ฝ๋๋ก ๋ณํํ๋ ๋ฐฉ์์ผ๋ก ๋์ํด์.
AST Explorer
AST Explorer์์ ์ฝ๋๋ฅผ ๋ถ์ฌ๋ฃ์ผ๋ฉด AST ๊ตฌ์กฐ๋ฅผ ์๊ฐ์ ์ผ๋ก ํ์ธํ ์ ์์ด์. Transform ์ต์ ์์ jscodeshift๋ฅผ ์ ํํ๋ฉด ๋ณํ ๊ฒฐ๊ณผ๋ ์ค์๊ฐ์ผ๋ก ํ์ธํ ์ ์์ด์.
jscodeshift ์ค์น
jscodeshift๋ Facebook์ด ๋ง๋ codemod ์คํ ๋๊ตฌ์์. ๋ด๋ถ์ ์ผ๋ก recast๋ฅผ ์ฌ์ฉํด ์๋ณธ ์ฝ๋์ ํฌ๋งคํ ์ ์ต๋ํ ๋ณด์กดํด์.
# jscodeshift ์ค์น
pnpm add -D jscodeshift @types/jscodeshifttransform ํจ์์ ๊ธฐ๋ณธ ๊ตฌ์กฐ
๋ชจ๋ codemod๋ ํ๋์ transform ํจ์๋ก ์ด๋ฃจ์ด์ ธ ์์ด์.
// transforms/my-transform.ts
import type { API, FileInfo } from "jscodeshift";
export default function transformer(fileInfo: FileInfo, api: API) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// AST ๋ณํ ๋ก์ง ์์ฑ
return root.toSource();
}์ธ ๊ฐ์ง ํต์ฌ ์์๊ฐ ์์ด์.
fileInfo.source- ๋ณํ ๋์ ํ์ผ์ ์์ค ์ฝ๋ ๋ฌธ์์ดapi.jscodeshift(๋ณดํตj๋ก ์ถ์ฝ) - AST ๋ ธ๋๋ฅผ ์ฐพ๊ณ ์์ ํ๋ APIroot.toSource()- ์์ ๋ AST๋ฅผ ๋ค์ ์์ค ์ฝ๋ ๋ฌธ์์ด๋ก ๋ณํ
์ฒซ ๋ฒ์งธ Codemod ์์ฑํ๊ธฐ
๊ฐ๋จํ ์์ ๋ก ์์ํด๋ณผ๊ฒ์. ํ๋ก๋์
์ฝ๋์์ console.log๋ฅผ ์ ๊ฑฐํ๋ codemod๋ฅผ ๋ง๋ค์ด ๋ด์.
console.log ์ ๊ฑฐ codemod
// transforms/remove-console-log.ts
import type { API, FileInfo } from "jscodeshift";
export default function transformer(fileInfo: FileInfo, api: API) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// console.log ํธ์ถ์ ์ฐพ์์ ์ ๊ฑฐ
root
.find(j.ExpressionStatement, {
expression: {
type: "CallExpression",
callee: {
object: { name: "console" },
property: { name: "log" },
},
},
})
.remove();
return root.toSource();
}์ด ์ฝ๋์ ๋์ ํ๋ฆ์ ์ดํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์์.
j(fileInfo.source)๋ก ์์ค ์ฝ๋๋ฅผ AST๋ก ํ์ฑํด์.find()๋กconsole.log()ํธ์ถ์ ํด๋นํ๋ ๋ ธ๋๋ฅผ ์ฐพ์์.remove()๋ก ํด๋น ๋ ธ๋๋ฅผ ์ ๊ฑฐํด์.toSource()๋ก ์์ ๋ AST๋ฅผ ์ฝ๋๋ก ๋ณํํด์
์คํ๊ณผ ๊ฒฐ๊ณผ ํ์ธ
--dry์ --print ์ต์
์ผ๋ก ์ค์ ํ์ผ์ ์์ ํ์ง ์๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ฏธ๋ฆฌ ํ์ธํ ์ ์์ด์.
# ๋ณ๊ฒฝ ์ฌํญ ๋ฏธ๋ฆฌ ํ์ธ (ํ์ผ ์์ ์์)
npx jscodeshift -t transforms/remove-console-log.ts src/ --dry --print
# ์ค์ ์คํ
npx jscodeshift -t transforms/remove-console-log.ts src/ --parser=tsx์ฃผ์
codemod๋ฅผ ์คํํ๊ธฐ ์ ์ ๋ฐ๋์ Git์ ์ปค๋ฐํ์ธ์. --dry --print๋ก ๊ฒฐ๊ณผ๋ฅผ
ํ์ธํ ๋ค, ์ค์ ์คํ ํ์๋ git diff๋ก ๋ณ๊ฒฝ ์ฌํญ์ ๊ฒํ ํ๋ ๊ฒ์ด ์์ ํด์.
์ค์ Codemod - API ๋ง์ด๊ทธ๋ ์ด์
์ค๋ฌด์์ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉํ๋ codemod ํจํด์ API ๋ง์ด๊ทธ๋ ์ด์ ์ด์์. deprecated๋ ํจ์๋ฅผ ์ ํจ์๋ก ๋ฐ๊พธ๋ ์์ ๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์.
์๋๋ฆฌ์ค
fetchData(url, callback) ํํ์ ์ฝ๋ฐฑ ๊ธฐ๋ฐ API๋ฅผ fetchData(url, options) ํํ์ ์ต์
๊ฐ์ฒด ํจํด์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์
ํ๋ค๊ณ ๊ฐ์ ํด์.
๋ณํ ์
// ๊ธฐ์กด ์ฝ๋
fetchData("/api/users", (err, data) => {
if (err) handleError(err);
setUsers(data);
});๋ณํ ํ
// ๋ณํ๋ ์ฝ๋
fetchData("/api/users", {
onSuccess: (data) => {
setUsers(data);
},
onError: (err) => {
handleError(err);
},
});๋ง์ด๊ทธ๋ ์ด์ codemod
// transforms/migrate-fetch-data.ts
import type { API, FileInfo } from "jscodeshift";
export default function transformer(fileInfo: FileInfo, api: API) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
root
.find(j.CallExpression, {
callee: { name: "fetchData" },
})
.filter((path) => {
// ๋ ๋ฒ์งธ ์ธ์๊ฐ ํ์ดํ ํจ์์ธ ๊ฒฝ์ฐ๋ง ๋ณํ
const args = path.node.arguments;
return args.length === 2 && args[1].type === "ArrowFunctionExpression";
})
.replaceWith((path) => {
const [urlArg, callbackArg] = path.node.arguments;
if (callbackArg.type !== "ArrowFunctionExpression") return path.node;
const params = callbackArg.params;
const errParam = params[0];
const dataParam = params[1];
// ์ฝ๋ฐฑ ๋ณธ๋ฌธ์์ ์๋ฌ ์ฒ๋ฆฌ์ ์ฑ๊ณต ๋ก์ง ๋ถ๋ฆฌ
const body = callbackArg.body;
if (body.type !== "BlockStatement") return path.node;
const statements = body.body;
const errorStatements = statements.filter(
(stmt) =>
j(stmt).toSource().includes(j(errParam).toSource()) &&
!j(stmt).toSource().includes(j(dataParam).toSource()),
);
const successStatements = statements.filter(
(stmt) => !errorStatements.includes(stmt),
);
// ์๋ก์ด ์ต์
๊ฐ์ฒด ์์ฑ
const optionsObj = j.objectExpression([
j.property(
"init",
j.identifier("onSuccess"),
j.arrowFunctionExpression(
dataParam ? [dataParam] : [],
j.blockStatement(successStatements),
),
),
j.property(
"init",
j.identifier("onError"),
j.arrowFunctionExpression(
errParam ? [errParam] : [],
j.blockStatement(errorStatements),
),
),
]);
return j.callExpression(j.identifier("fetchData"), [urlArg, optionsObj]);
});
return root.toSource();
}์ด codemod๋ fetchData์ ๋ ๋ฒ์งธ ์ธ์๊ฐ ์ฝ๋ฐฑ ํจ์์ธ ๊ฒฝ์ฐ๋ฅผ ์ฐพ์ ์ต์
๊ฐ์ฒด ํจํด์ผ๋ก ๋ณํํด์. .filter()๋ก ๋ณํ ๋์์ ์ ํํ ์ขํ๊ณ , .replaceWith()๋ก ์๋ก์ด AST ๋
ธ๋๋ฅผ ์์ฑํ๋ ํจํด์ด์์.
๋ณต์กํ codemod๋ฅผ ์์ฑํ ๋๋ AST Explorer์์ ๋ณํ ์ /ํ ์ฝ๋์ AST ๊ตฌ์กฐ๋ฅผ ๋น๊ตํ๋ฉด์ ์์ ํ๋ ๊ฒ์ด ํจ์จ์ ์ด์์.
Codemod ํ ์คํธ์ ๋๋ฒ๊น
codemod๋ ์ฝ๋๋ฅผ ์๋์ผ๋ก ๋ณ๊ฒฝํ๋ ๋๊ตฌ์ด๊ธฐ ๋๋ฌธ์, ํ ์คํธ ์์ด ์คํํ๋ฉด ์์์น ๋ชปํ ๊ฒฐ๊ณผ๊ฐ ๋์ฌ ์ ์์ด์.
jscodeshift ํ ์คํธ ์ ํธ๋ฆฌํฐ
jscodeshift๋ Jest ๊ธฐ๋ฐ ํ ์คํธ ์ ํธ๋ฆฌํฐ๋ฅผ ์ ๊ณตํด์. ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ๋ง์ถ๋ฉด ๊ฐ๋จํ๊ฒ ํ ์คํธ๋ฅผ ์์ฑํ ์ ์์ด์.
transforms/
โโโ remove-console-log.ts
โโโ __tests__/
โ โโโ remove-console-log.test.ts
โโโ __testfixtures__/
โโโ remove-console-log.input.ts
โโโ remove-console-log.output.ts// transforms/__testfixtures__/remove-console-log.input.ts
const value = 42;
console.log("debug:", value);
doSomething(value);// transforms/__testfixtures__/remove-console-log.output.ts
const value = 42;
doSomething(value);// transforms/__tests__/remove-console-log.test.ts
import { defineTest } from "jscodeshift/src/testUtils";
defineTest(__dirname, "remove-console-log", null, "remove-console-log", {
parser: "tsx",
});defineTest๋ input ํ์ผ์ transform์ ํต๊ณผ์ํจ ๊ฒฐ๊ณผ๊ฐ output ํ์ผ๊ณผ ์ผ์นํ๋์ง ์๋์ผ๋ก ๊ฒ์ฆํด์.
์ฃ์ง ์ผ์ด์ค ์ฒ๋ฆฌ
์ค์ ์ฝ๋๋ฒ ์ด์ค์๋ ๋ค์ํ ํจํด์ด ์กด์ฌํด์. ๋ํ์ ์ธ ์ฃ์ง ์ผ์ด์ค๋ฅผ ๋ฏธ๋ฆฌ ๊ณ ๋ คํด์ผ ํด์.
console.log๊ฐ ์๋console.warn,console.error- ๋ณ์์ ํ ๋น๋
const log = console.logํจํด - ์ต์
๋ ์ฒด์ด๋
console?.log()ํจํด - ์ฃผ์์ด ํฌํจ๋ ์ฝ๋
--dry-run์ผ๋ก ์์ ํ๊ฒ ํ์ธ
codemod ์คํ ์ ๋ฐ๋์ ์์ ์ ์ฐจ๋ฅผ ๊ฑฐ์น์ธ์.
# 1๋จ๊ณ. ๋ณ๊ฒฝ๋ ํ์ผ ๋ชฉ๋ก ํ์ธ
npx jscodeshift -t transforms/remove-console-log.ts src/ --dry
# 2๋จ๊ณ. ๋ณ๊ฒฝ ๋ด์ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
npx jscodeshift -t transforms/remove-console-log.ts src/ --dry --print
# 3๋จ๊ณ. ์คํ (ํ์ ์ง์ )
npx jscodeshift -t transforms/remove-console-log.ts src/ --parser=tsx --extensions=ts,tsx
# 4๋จ๊ณ. Git diff๋ก ๊ฒฐ๊ณผ ๊ฒํ
git diff์ ์ฉํ CLI ์ต์
--verbose=2๋ก ์์ธ ๋ก๊ทธ๋ฅผ ํ์ธํ๊ณ , --run-in-band๋ก ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ฅผ ๋๋ฉด
๋๋ฒ๊น
์ด ์ฌ์์ ธ์. --ignore-pattern=**/node_modules/**๋ก ๋ถํ์ํ ํ์ผ์
์ ์ธํ ์ ์์ด์.
๋ง๋ฌด๋ฆฌ
codemod๋ฅผ ํ์ฉํ ๋ฆฌํฉํ ๋ง์ ํต์ฌ์ ์ ๋ฆฌํ๋ฉด ๋ค์๊ณผ ๊ฐ์์.
- AST ๊ธฐ๋ฐ ๋ณํ - ์ ๊ท์๊ณผ ๋ฌ๋ฆฌ ์ฝ๋์ ๊ตฌ์กฐ์ ์๋ฏธ๋ฅผ ์ดํดํ๋ฏ๋ก ์์ ํ ๋ณํ์ด ๊ฐ๋ฅํด์
- jscodeshift -
find()โfilter()โreplaceWith()โtoSource()ํจํด์ผ๋ก codemod๋ฅผ ์์ฑํด์ - AST Explorer ํ์ฉ - ๋ณํ ์ /ํ ์ฝ๋์ AST ๊ตฌ์กฐ๋ฅผ ๋น๊ตํ๋ฉฐ ๊ฐ๋ฐํ๋ฉด ํจ์จ์ ์ด์์
- ํ ์คํธ ํ์ - input/output fixture ๊ธฐ๋ฐ ํ ์คํธ๋ก ์ฃ์ง ์ผ์ด์ค๋ฅผ ๊ฒ์ฆํด์
- ์์ ํ ์คํ -
--dry --print๋ก ๋ฏธ๋ฆฌ ํ์ธํ๊ณ , Git diff๋ก ๊ฒฐ๊ณผ๋ฅผ ๊ฒํ ํด์
ํ๋ก์ ํธ์์ ๋ฐ๋ณต์ ์ธ ์ฝ๋ ๋ณ๊ฒฝ ์์ ์ด ์๋ค๋ฉด codemod๋ฅผ ์์ฑํด๋ณด์ธ์. ์ฒ์์๋ AST ๊ตฌ์กฐ๊ฐ ๋ฏ์ค ์ ์์ง๋ง, AST Explorer์ ํจ๊ป ํ๋์ฉ ๋ง๋ค์ด๋ณด๋ฉด ๊ธ๋ฐฉ ์ต์ํด์ ธ์.
์ฐธ๊ณ ์๋ฃ
Afaik ยฉ 2025
์ธ๋ถ ๋งํฌ