
이번 주차 과제는 정상 동작은 하지만, 코드의 컨벤션이 정해지지 않은, 들여쓰기나 띄어쓰기, 변수와 함수명 등이 일관성이 없는 상태의 코드를 다른 사람이 보더라도 별도의 설명없이 로직을 이해할 수 있는 코드로 리팩토링하는 것을 목표하였습니다.
클린코드
- 의도가 분명한 네이밍 룰
- 변수나 함수 이름만으로도 '왜 존재하는 지', '무엇을 하는 지' 명확해야합니다.
- 작고 단순한 함수
- 하나의 함수는 하나의 일만 하도록 작성하고,
- 길거나 여러가지 작업을 반영하면 읽기도, 테스트 하기도 어려줘 집니다.
- 중복 제거
- 중복된 코드는 하나의 함수로 분리하여 유지보수에 용이하게 합니다.
- 의미있는 주석 또는 주석이 필요없는 코드
- 코드만으로도 설명이 가능하는게 좋으며,
- 설명하더라도 '무엇' 보다는 '왜'에 대하여 설명해야합니다.
- 에러 처리 단순화
try-catch블록은 최소로하고, 에러 상황은 명확하게 처리해야합니다.- 포맷과 스타일의 일관성
- 들여쓰기, 줄바꿈, 공백 등은 일관되게 유지해야합니다.
먼저, 포맷 스타일의 일관성을 보다 쉽게 적용하기 위해 ESLint와 Prettier를 활용하였습니다.
ESLint란?
ESLint는 JavaScript/TypeScript 코드의 문법 오류, 안티 패턴, 잠재적 버그를 사전에 잡아주는 정적 분석 도구입니다.
- 잘못된 변수 사용 탐지
- 사용하지 않는 변수 경고
- 코드 스타일 규칙 적용 (예: var 금지, const 사용 권장)
- 팀의 코드 컨벤션 유지
- 주요 기능
- 문법 오류 감지: const를 let로 바꿔야 하는 경우
- 코드 스타일 규칙 적용: 들여쓰기, 따옴표 통일 등
- 플러그인 확장 가능: React, TypeScript, Prettier 등과 통합 가능
// eslint.config.js import js from "@eslint/js"; import eslintPluginPrettier from "eslint-plugin-prettier/recommended"; import eslintConfigPrettier from "eslint-config-prettier"; export default [ js.configs.recommended, eslintPluginPrettier, // prettier 룰 적용 eslintConfigPrettier, // prettier와 충돌하는 룰 제거 { rules: { "no-var": "error", // var 금지 "prefer-const": "warn", // const 권장 "no-unused-vars": "warn", // 미사용 변수 경고 "prettier/prettier": "warn", // prettier 규칙 경고로 출력 }, }, ];
Prettier란?
Prettier는 코드의 형식을 자동으로 정리해주는 포매터입니다. ESLint와 달리 오류를 검사하지 않고, 형식만 통일합니다.
- 세미콜론, 들여쓰기, 따옴표 등 자동 정리
- 코드 스타일 논쟁 해결
- 저장 시 자동 포맷 가능
{ "semi": true, "singleQuote": false, "printWidth": 100, "tabWidth": 2, "trailingComma": "es5" }
주석이 필요없는 코드가 클린코드의 조건이지만, 과제로 나온 코드가 정확한 역할이 구분이 되지 않아, 중간 정리 개념으로 함수별 JSDoc을 작성하였습니다.
주요 태그 설명
@param함수의 매개변수 타입과 설명
@returns함수의 반환값 타입과 설명
@const상수에 대한 설명
@type변수의 타입 명시
@example사용 예시
@typedef커스텀 타입 정의
특히나, 업무에서도 한번 썼던 커스텀 JQuery함수를 이번 바닐라JS 코드 상에서도 활용해보기 위해 해당 함수가 어떤 역할을 하고, 무엇을 반환해주는 지, props와 children의 처리는 어떻게 동작하는 지에 대하여 JSDoc를 활용하여 서술하였습니다.
/**
* 지정된 태그 혹은 선택자에 따라 적절한 DOM 노드를 반환
*
* - `"#id"` 또는 `".class"`로 시작하면 `document.querySelector`를 사용해 DOM을 조회
* - `"frag"` 문자열을 전달하면 `DocumentFragment`를 반환
* - 그 외에는 일반 DOM 요소를 생성하며, 전달된 `props`를 속성으로 할당
*
* @param {string} type
* @param {Object} [props]
* @param {string} [props.id]
* @param {string | string[]} [props.className]
* @param {string} [props.textContent]
* @param {string} [props.value]
* @param {boolean} [props.disabled]
* @param {Object<string, string>} [props.dataset] data-* 오브젝트
* @param {...(HTMLElement | Text | DocumentFragment)} children
* @returns {HTMLElement | DocumentFragment | null} DOM 엘리먼트
*
* @example
* const div = $("div", { className: "bg-gray-100 p-8", dataset: { productId: "p1" } });
* const option = $("option", { value: "p1", textContent: "상품1", disabled: false });
* const frag = $("frag");
* const existingEl = $("#app"); // querySelector
*/
export const $ = (type, props = {}, ...children) => {
// querySelector
if (type.startsWith("#") || type.startsWith(".")) {
return document.querySelector(type);
}
// fragment 생성
if (type === "frag") {
return document.createDocumentFragment();
}
// 일반 element 생성
const element = document.createElement(type);
Object.entries(props || {}).forEach(([key, value]) => {
if (key === "dataset" && typeof value === "object") {
// dataset 처리
Object.entries(value).forEach(([dataKey, dataValue]) => (element.dataset[dataKey] = dataValue));
} else {
// id, className 및 기타 속성 처리
if (key === "className" && Array.isArray(value)) value = value.join(" ");
element[key] = value;
}
});
// element children 생성 시 추가
if (children && Array.isArray(children)) {
children.filter((c) => !!c && c instanceof Node).forEach((el) => element.appendChild(el));
}
return element;
};
props만을 정의해두고, 점차 dataset이나 children을 추가하여 React.createElement의 동작과 비슷하게 실제 DOM을 조작하는 함수를 함께 구현하였습니다.
다음 심화과제 이전에 팀원들과 코드 리뷰를 진행해보고, 더 나은 방법으로 통합하여 심화과제를 진행할 예정입니다. 심화과제는 현재 구현되어있는 코드를 동일한 동작을 할 수 있도록 React로 구현하는 것인데, 이 또한 기대가 되는 과제입니다.
현재 회사에서도 전임자들이 짜놓은 바닐라JS로 되어있는 코드가 50만줄을 넘어가고 있어서, 슬슬 리팩토링 해야하는 상황인데, 이번 챕터를 기반으로 실무에 반영해볼 수 있는 기회가 될 것 같아 더욱 기대가 되고 있습니다.