jQuery.
너와 처음 만났을 땐, 모든 게 편하고 쉬웠어.
그저 $(...)만 써도 뭐든 다 됐으니까.
하지만 시간이 흐르면서
너 없는 세상도 점점 괜찮아졌고,
이젠… 우리, 각자의 길을 가야 할 때야.
고마웠어 jQuery. 정말 많은 걸 배웠어.
그리고...
다시는 보지 말자. 진심이야.
1. 시작은 보안 이슈였다
기존 솔루션에 사용되던 jQuery는 1.7.2.
보안 취약점이 발견되어 3.5.1 이상으로 올리는 게 권장되었다.
그런데 문득 이런 생각이 들었다.
"이참에 jQuery 자체를 걷어내볼까?"
단순한 버전 업그레이드 대신
jQuery 의존성을 걷어내고 모듈 기반 구조로 바꿔보자는 시도였다.
2. 왜 jQuery를 걷어내기로 했는가?
🧭 jQuery는 한때 ‘표준’이었다
2006년, jQuery는 “Write less, do more”라는 철학으로 등장했다.
그 시절은 document.getElementById만으로는 역부족이었고
브라우저 간 DOM API는 파편화되어 있었다.
- addEventListener는 IE9부터 지원
- querySelector는 IE8조차 미지원
- 이벤트 바인딩, DOM 탐색, CSS 클래스 토글 등은 브라우저별로 코드가 달랐다
jQuery는 이 불일치를 추상화해줬다.
그 덕분에 개발자들은 브라우저 호환성 걱정 없이 개발할 수 있었다.
// 과거에는 addEventListener조차 안 되는 브라우저가 있었다
element.addEventListener("click", handler); // IE9 미만에서 X
→ 그래서 우리는 이렇게 썼다.
// jQuery라면 IE6에서도 문제없음
$(".btn").on("click", handler);
🔁 하지만 시대가 바뀌었다
2020년 기준으로 대부분의 브라우저는 ES6 이상을 지원한다.
기능 | Chrome | Firefox | Safari | Edge | IE |
---|---|---|---|---|---|
addEventListener | ✔️ | ✔️ | ✔️ | ✔️ | ❌ (IE8 이하) |
querySelector | ✔️ | ✔️ | ✔️ | ✔️ | ❌ (IE7 이하) |
classList | ✔️ | ✔️ | ✔️ | ✔️ | IE10부터 |
fetch | ✔️ | ✔️ | ✔️ | ✔️ | ❌ (IE 전부) |
즉, jQuery가 제공하던 기능 중 90% 이상이 표준 JS로 대체 가능해졌다.
jQuery의 가장 큰 존재 이유였던 “호환성 해결”의 필요성이 사라진 셈이다.
💡참고:
현재 대부분의 서비스는 IE11 지원조차 중단하고 있으며
만약 IE 하위 버전(IE9 이하)을 지원해야 한다면
일부 기능은 폴리필(polyfill)이 필요하다.
그러나 신규 서비스라면
굳이 그 레거시를 짊어질 이유는 거의 없다.
💡 그래서 굳이 jQuery를 쓸 필요가 있을까?
몇 줄의 편리를 위해 30KB 짜리 라이브러리를 유지해야 할까?
- jQuery 3.5.1 minified 기준 약 32KB
- 대부분의 사용이 addClass, on, val, hide/show 등 단순 조작
- 번들링/ Tree Shaking 시대에서 불필요한 전역 의존성은 관리 리스크
// jQuery
$("#favorite_id").val("");
// JS
document.getElementById("favorite_id").value = "";
🧱 구조화와 재사용도 고려했다
jQuery는 "빠르고 간단한 DOM 조작"이라는 장점이 있지만,
모듈화/ 재사용 관점에서는 취약한 구조다.
❌ jQuery 시절의 문제점
- 전역 의존성
- $는 전역 객체로 어디서든 접근 가능하지만
- 반대로 말하면 어디서 무슨 DOM을 조작하는지 추적이 어렵다.
- 명확한 책임 분리가 어려움
- UI 렌더링, 이벤트 바인딩, 상태 변경이 한 함수에 섞여 있는 경우가 많다.
- 테스트 가능한 구조로 만들기 어려움.
- 기능별 파일 분리 어려움
- jQuery 기반 코드는 HTML과 강하게 결합되어 있어서
- 특정 DOM 요소가 바뀌면 전체 코드가 영향을 받기 쉬움.
// jQuery는 이런 식으로 동작을 HTML 구조에 밀접하게 결합시킴
$(".favoriteBox .item .close").on("click", function () {
$(this).closest(".item").remove();
});
→ 이 구조는 .favoriteBox나 .item이 구조에서 사라지면 전부 깨진다.
✅ 순수 JavaScript 전환 후 얻은 것
1. DOM 접근 함수화 → 변경에 유연한 구조
// DOM 유틸 함수 예시
function getEl(id) {
return document.getElementById(id);
}
function hide(el) {
el.style.display = "none";
}
function show(el) {
el.style.display = "block";
}
- 모든 DOM 조작 로직을 함수로 추상화해서
- 추후 구조가 변경되더라도 해당 함수만 수정하면 된다.
2. 기능 단위 모듈화
// favorite.js
import { getEl, show, hide } from "./dom-utils.js";
export function bindFavoriteToggle() {
const favEl = getEl("favorite_id");
const listEl = getEl("favorite_list");
favEl.addEventListener("click", () => {
show(listEl);
setTimeout(() => hide(listEl), 10000);
});
document.addEventListener("mouseup", (e) => {
if (!document.querySelector(".favoriteBox").contains(e.target)) {
hide(listEl);
}
});
}
- 기능별로 파일 분리
- 공통 DOM 조작은 유틸화
- 한눈에 흐름이 보이고 유지보수가 쉬움
3. 테스트 가능한 구조로 개선
- UI 로직 외에 계산/판단/조건 분기를 분리 가능
- 유닛 테스트가 가능한 pure function으로 만들 수 있음
export function isValidCount(value) {
return value >= 10 && value <= 1000;
}
→ 이건 테스트 코드에서 다음과 같이 검증 가능
test("isValidCount", () => {
expect(isValidCount(5)).toBe(false);
expect(isValidCount(100)).toBe(true);
});
4. 명시적인 의존성과 정적 분석 호환성
- jQuery 기반 코드는 $().on, $().each 같은 문법이 전역에 걸쳐 사용되며 DOM 요소와 이벤트 흐름을 추적하기 어렵다.
- 순수 JavaScript 기반 코드로 바꾸면 eslint, tsc, vite 등의 정적 분석 도구와 잘 연동되어 에러 탐지, 리팩토링, 검색이 쉬워진다.
- 전체 코드베이스에 일관성과 예측 가능성을 부여할 수 있다.
🧨 그리고 가장 큰 문제는 "혼용"
이 프로젝트는 jQuery 1.7.2 → 3.5.1로 업그레이드하는 과정이었지만
과거 코드와 새로운 jQuery 코드가 혼재되어 있었다.
- $().on()이 먹히지 않거나
- $().fadeIn()이 동작이 달라지거나
- 버전별로 이벤트 위임이 미묘하게 달라지거나
즉, 동작 예측이 어려워졌다.
→ 이건 명확한 기술 부채였다.
✅ 그래서 결론은 단순했다
- 브라우저가 성장했기 때문에
- 내가 제어할 수 있는 JS 코드를 만들기 위해
- 의존성과 리스크를 줄이기 위해
“걷어내자”
이제는 표준 JavaScript로도 충분하니까.
🧠 3. 기술적 고민들
순수 JavaScript로 전환한다고 해서 단순히 $('#el')을 getElementById('el')로 바꾸는 작업만 있었던 건 아니다.
의도적으로 ‘제대로 바꾸자’고 마음먹은 순간부터
생각보다 많은 선택지가 눈앞에 펼쳐졌다.
1️⃣ addEventListener vs onclick
🟡 고민한 이유
- 기존 jQuery에서는 .on("click", fn) 방식이 주였고
- JS에서도 onclick = fn과 addEventListener("click", fn) 두 방식이 모두 가능했다
🟢 최종 선택: addEventListener
항목 | onclick | addEventListener |
---|---|---|
중복 등록 | ❌ 마지막 이벤트만 남음 | ✅ 여러 핸들러 등록 가능 |
제거 용이성 | ❌ 일부만 제거 불가 | ✅ removeEventListener로 제거 가능 |
일관된 관리 | ❌ HTML에 섞여있기도 함 | ✅ JS 내부에서만 선언 가능 |
→ 모듈화 & 유지보수 측면에서 addEventListener가 압도적이었다.
2️⃣ DOM 탐색과 조작의 함수화
🟡 문제
- jQuery는 DOM 탐색/조작을 매우 간단하게 해주지만
- 그만큼 중복 로직과 하드코딩이 발생하기 쉬움
// jQuery 예시
$(".favoriteBox .item .close").on("click", function () {
$(this).closest(".item").remove();
});
→ 이 구조는 HTML 구조가 조금만 바뀌어도 망가짐
🟢 해결: 공통 DOM 접근 & 조작을 함수화
function getEl(selector) {
return document.querySelector(selector);
}
function hide(el) {
el.style.display = "none";
}
- 접근 방식이 한 곳에 집중되어 변경에 유연
- document.querySelector나 getElementById처럼 표준 함수 기반으로 통일
3️⃣ fadeIn / fadeOut → CSS 전환 + JS 타이머 제어
jQuery의 fadeIn(), fadeOut()은 편리했지만
순수 JS에선 CSS 전환과 JS 타이머를 조합해야 했다.
그래서 opacity, transition, setTimeout을 조합해 대체했다.
// jQuery
$('#msg').fadeIn().delay(1000).fadeOut();
// 순수 JavaScript 대체 코드
const el = document.getElementById("msg");
el.style.opacity = "1";
setTimeout(() => {
el.style.opacity = "0";
}, 1000);
- CSS에 transition 정의 후 opacity만 조작
- JavaScript에서 타이머를 분리해 UX 흐름 제어
#msg {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
4️⃣ 재사용 가능한 유틸과 로직 분리
기존 jQuery 코드에서는 복붙이 많았다.
→ 순수 JS 전환을 기점으로 공통 로직을 유틸로 추출했다.
// validate.js
export function isValidCount(val) {
return Number(val) >= 10 && Number(val) <= 1000;
}
- 어떤 값 검증, 포커스 제어, DOM 표시 등은
- 하나의 유틸 함수로 만들어서 여러 모듈에서 재사용하게 설계
5️⃣ JS 모듈 시스템 활용
- ES Modules(import/export) 기반으로 각 기능을 분리
- jQuery 시절에는 묶기 어려웠던 로직들을 기능별로 파일로 나누는 기회가 됨
// favorite.js
export function bindFavoriteToggle() { ... }
// main.js
import { bindFavoriteToggle } from "./favorite.js";
bindFavoriteToggle();
3.5 함수 선언 방식에 대한 고민: function vs =>
순수 JavaScript로 리팩토링을 하다 보니 단순한 문법 변환 외에도
“어떤 방식으로 함수를 선언할 것인가?” 에 대한 고민이 뒤따랐다.
기존에는 대부분 이렇게 되어 있었다.
function(ev) {
console.log(this); // 클릭된 요소
}
그러나 ES6 이후에는 아래처럼 화살표 함수(Arrow Function)를 더 많이 쓰는 추세다.
const handler = (ev) => {
console.log(ev.target); // 명시적 대상
}
📌 바꾼 이유 1: this 컨텍스트의 명확성
- function 선언에서는 this가 실행 컨텍스트에 따라 달라진다.
- 특히 DOM 이벤트 핸들러에서는 this가 이벤트 대상 요소를 참조한다.
function(ev) {
console.log(this); // 클릭된 input, div 등
}
- 반면, 화살표 함수에서는 this가 상위 스코프에 고정된다.
const handler = (ev) => {
console.log(this); // 예상치 못한 this (ex. window)
}
→ 그래서 this를 써야 하는 경우라면 화살표 함수는 부적절하다.
그러나 이번 리팩토링에서는 대부분 ev.target을 명시적으로 사용했기 때문에 this 충돌은 크지 않았다.
📌 바꾼 이유 2: 함수 스코프와 가독성 개선
- const fn = () => {} 방식은 변수처럼 다룰 수 있어 정렬과 구조화에 유리하다.
- 상단에서 선언해두고 필요한 곳에서 자연스럽게 사용할 수 있다.
- 호이스팅을 방지할 수 있다는 점도 유지보수에 도움이 된다.
const toggleElement = (id) => {
const el = document.getElementById(id);
el.style.display = el.style.display === "none" ? "block" : "none";
};
📌 바꾼 이유 3: UI 로직을 파일 단위로 분리하기 쉬움
기능별 모듈로 나누면서 함수 단위 export가 많아졌고,
이 때 화살표 함수는 일관성과 재사용 측면에서 장점이 많았다.
// popup.js
export const openPopup = () => { ... };
export const closePopup = () => { ... };
- 다른 파일에서 import할 때 간결
- 콜백 함수로 전달해도 this가 꼬이지 않음
- 테스트 작성 시도 간편함
❗️주의할 점: 반복문, setTimeout, 동적 핸들러
- 반복문 안에서 이벤트 핸들러를 등록하거나
- setTimeout/setInterval 내부에서 this를 사용하는 경우
→ 화살표 함수의 this는 바깥 스코프를 따라가기 때문에 주의가 필요하다.
setTimeout(() => {
this.doSomething(); // this가 예상대로 작동할 수도, 아닐 수도
}, 1000);
🧠 추가로 고려할 사항: arguments, new 사용
- arguments 객체 사용 불가
- 화살표 함수는 자신만의 arguments 객체를 가지지 않는다.
- 호출 컨텍스트의 arguments를 참조하거나 반드시 rest 파라미터(...args)를 써야 한다.
const logArgs = () => { console.log(arguments); // ❌ 에러: arguments is not defined }; const logArgsFix = (...args) => { console.log(args); // ✅ ['a', 'b'] };
- new 키워드와 함께 사용 불가
- 화살표 함수는 constructor로 사용할 수 없다.
- 즉, new ArrowFunction() 형태는 TypeError를 발생시킨다.
const Person = (name) => { this.name = name; }; const p = new Person("IU"); // ❌ TypeError: Person is not a constructor function PersonFunc(name) { this.name = name; } const p2 = new PersonFunc("IU"); // ✅ 정상 작동
=> 함수는 콜백, 이벤트 핸들러, 유틸 함수엔 유용하지만
this, arguments, new가 필요한 문맥에선 전통적인 function 선언이 적합하다.
4. 리팩토링 결과와 회고
✅ 1) 의존성 제거: jQuery, 이젠 안녕
한때 jQuery는 웹 개발의 대세였다.
브라우저 간 호환성도 잡아주고, 복잡한 DOM 조작을 몇 줄로 끝내주는 강력한 도구였다.
하지만 지금은 시대가 달라졌다.
- 대부분의 브라우저가 ES6+ 표준을 완전히 지원하고
- document.querySelector, addEventListener, fetch 등
- jQuery 없이도 충분한 API들이 이미 기본 제공되고 있다.
그런데도 레거시 프로젝트엔 아직도 이런 코드들이 남아 있었다.
j$("#favorite_list").show();
j$(document).mouseup(function(e) {
if (j$(".favoriteBox").has(e.target).length == 0) {
j$('#favorite_list').hide();
}
});
기능은 단순했지만 jQuery 없이도 충분히 처리할 수 있는 영역이었다.
오히려 작은 기능을 위해 30KB가 넘는 라이브러리를 유지하고 있다는 점이 아쉬웠다.
// 순수 JavaScript로 대체
document.getElementById("favorite_list").style.display = "block";
document.addEventListener("mouseup", function(e) {
if (!document.querySelector(".favoriteBox").contains(e.target)) {
document.getElementById("favorite_list").style.display = "none";
}
});
의존성을 걷어낸다는 건
단순히 라이브러리를 없애는 일이 아니라
불필요한 추상화를 걷어내고 본질로 돌아가는 과정이었다.
물론 jQuery는 여전히 강력한 도구다.
하지만 전체 코드에서 그 기능이 필요한 부분은 점점 줄어들고 있었고
그 몇 줄 때문에 무거운 짐을 계속 지고 갈 순 없었다.
2) DOM 접근과 로직을 구조화
jQuery 시절에는 모든 게 너무 쉬웠다.
j$("#some_id").val();
j$(".popup").hide();
하지만 이 “쉬움”이 문제를 감췄다.
HTML 구조에 너무 쉽게 접근할 수 있었기에
비즈니스 로직과 화면 제어가 한데 섞여버리는 경우가 많았다.
그래서 바꿔본 방식
jQuery를 걷어내며 자연스럽게 질문하게 됐다.
이 DOM 접근, 정말 여기서 직접 해야 할까?
"NO" 그래서 다음과 같은 기준으로 구조를 바꿨다.
📌 기준 1. DOM 접근은 함수로 추상화
// Before
document.getElementById("popup").style.display = "none";
// After
function hidePopup() {
document.getElementById("popup").style.display = "none";
}
작은 함수로 감싸기만 해도
- 네이밍으로 의도가 명확해지고
- 반복되는 DOM 조작을 줄일 수 있었다.
📌 기준 2. 역할별로 분리
- popup.js → 팝업 열고 닫기만 담당
- validation.js → 유효성 검사 모듈
이렇게 나누고 나니
프론트엔드도 결국 “레이어 분리”가 중요하구나 라는 걸 몸으로 느꼈다.
📌 기준 3. 재사용 가능한 유틸로 묶기
자주 쓰이는 DOM 제어 함수들은 domUtils.js라는 파일로 따로 빼서 관리했다.
// domUtils.js
export const showElement = (id) => {
document.getElementById(id).style.display = "block";
};
export const hideElement = (id) => {
document.getElementById(id).style.display = "none";
};
→ 이 작은 유틸이 코드의 중복을 줄이고 테스트를 쉽게 만들었고, 유지보수도 편해졌다.
3) 유지보수성 극대화: 구조화와 모듈화로 얻은 이점
기존 jQuery 기반 코드는 다음과 같은 문제를 안고 있었다.
- 여러 기능이 하나의 스크립트에 뒤엉켜 변경 범위가 넓었고
- DOM 조작과 상태 변경 로직이 섞여 있어 읽기 어렵고, 테스트가 불가능했으며
- jQuery 플러그인 또는 전역 이벤트 충돌로 예상치 못한 버그가 발생했다
그래서 단순히 문법을 바꾸는 수준이 아닌, 설계 레벨의 리팩토링을 진행했다.
✅ 해결 전략: 모듈화를 통한 리팩토링
jQuery를 걷어내는 작업과 함께, 역할 기반 모듈로 코드 구조를 재정비했다.
핵심은 관심사 분리(Separation of Concerns), 그리고 동적 로딩이었다.
그리고 모듈 로딩 방식은 import() 기반 동적 로딩(Dynamic Import) 으로 전환했다.
const common = await import(rootModule + '/common/common.js')
.catch(error => console.error('모듈 로드 실패:', error));
이 구조는 다음과 같은 이점을 가져왔다.
- 초기 로딩 성능 개선 → 필요한 시점에만 모듈을 로드해 번들 크기 최소화
- 조건부 분기 가능 → 특정 페이지나 기능에서만 모듈을 로드할 수 있음
- 공통 유틸 모듈의 재사용성 향상 → 기능별 유틸을 공통화하고 불필요한 중복 제거
이렇게 모듈화를 중심으로 한 리팩토링은
단순히 “코드가 깔끔해졌다” 수준을 넘어서프로젝트의 유지보수성, 협업 효율, 기능 확장성까지
확실히 향상시켜줬다.
🧾 회고: jQuery는 떠났지만, 고마웠다
처음엔 너 없인 아무것도 못 했어.
브라우저 호환이 깨지면 늘 네가 해결해줬고
몇 줄 안 되는 코드로 모든 게 마법처럼 됐으니까.
근데 이젠 너 없이도 잘 살아.
오히려… 더 잘 살아.
jQuery, 고마웠고...
지금은, 그래… 그냥 RIP.🪦
$ rm -rf jquery.min.js
너와의 추억은 깃허브 커밋에만 남겨둘게.