jQuery 탈출 가이드 #1 플러그인 도입을 시작하라

엽토군·2026년 3월 19일

jQuery 탈출 가이드

목록 보기
1/1

서문

2026년 3월 어느 날 이력서를 5군데 정도 더 집어넣어 놓고 잠을 자려는데 잠이 안 오더랬다. 억울해서. jQuery와 순수PHP를 쓰던 직전 직장에서도 그나마 배운 게 없지 않은데, 닥쳐오는 AI 시대 모든 것이 Typescript React로 수렴되면서 이 잔재주는 인류사에서 통째로 외면 망각될 것 같은 기분이 든다. 그 사실에 울화가 치밀어, 나라도 뭐라도 남겨놓고자 이 시리즈를 시작한다.

혹시 여러분이 운영 중인 레거시 프로젝트에서 jQuery가 골치를 썩이고 있는가?
탈출할 방법을 같이 생각하고 탈출하자.
일단 내가 시도했던 것들을 먼저 공유한다.

jQuery Plugin 시작하기

문제 상황: 재사용성의 부재

레거시 프로젝트에서 jQuery가 골칫거리가 되는 데는 여러 맥락이 있지만, 의외로 현실적으로 고통스러운 지점 중 하나가 "재사용성"의 부재다. 다른 곳에서는 캡슐화니 컴포넌트화니 복잡한 말이 많은데 jQuery가 짱 먹은 코드베이스에서 재사용성이란 종종 "복붙 편의성"을 의미하곤 하는 것이다.

실제로 있었던 사례 중에는 이런 것이 있다.
화면상의 어느 (아무것도 없는 듯한) 특정 부분을 5번 누르면 "숨겨진" 관리화면으로 넘어갈 수 있어야 한다고 해 보자.
왕년의 jQuery는 늠름하게 다음과 같이 코딩하고 보람차게 퇴근했다.

<style>
#gotoadmin {
  padding: 2em;
}
</style>
<button id="gotoadmin"></button>
<script>
  var adminclick = 0;
  $('#gotoadmin').click(function() {
    adminclick++;
    if (adminclick == 5) {
      window.navigation.href = '/admin';
    }
  });
</script>

그리고 1년쯤 지나서 관리자 화면에 "슈퍼관리기능"을 노출하는 "비슷한" 버튼이 하나 더 생겨야 한다고 해 보자.
후임자는 기존 코드를 복사해 쓰기로 결정한다.
조금 귀찮지만 재사용성이 없진 않군, 야근하지 않아도 돼서 다행이야, 하면서.

<style>
#showsuperadmin {
  padding: 2em;
}
</style>
<button id="showsuperadmin"></button>
<div id="superadmin" style="display: none;">슈퍼관리기능</div>
<script>
  var superadminclick = 0;
  $('#showsuperadmin').click(function() {
    superadminclick++;
    if (adminclick == 5) {
      $('#superadmin').show();
    }
  });
</script>

그런데 작동을 안 한다. 이런! 복붙하는 과정에서 한 군데 고쳐야 할 곳을 덜 고쳤다. (위의 코드 예제에 숨겨 놓았으니 한번 심심풀이로 찾아보시라!)
이 정도 실수/버그는 차라리 애교다. 담당자가 여럿 교체되면서 이런 버튼을 여기저기 복붙해서 달아 놨는데, 나중에 언젠가 "이 버튼들 5번 클릭 말고 10번 클릭으로 다 바꿔 주세요" 같은 요청이 들어온다면, 그땐 어떻게 할 것인가? 소스 코드 전체를 == 5로 검색해야겠지? 그렇게 고칠 것은 놓치고 안 망가진 것은 망가뜨리며 앞전 사람들이 안 한 몫까지 야근하는, 뒤늦은 기술 부채 상환이 벌어지는 것이다.

해결 방안: 일반화하여 플러그인으로 감싸기

위 사례의 명세는 다음과 같이 일반화 가능하다.

let count = 0;
let threshold = 5; // 달라질 수도 있으므로 변수화
let callback = function () { /* 달라질 수도 있으므로 변수화 */ };

$(btn).click(function () {
  count++;
  if (count == threshold) {
   	callback();
  }
});

이 프로시저를 함수로 감싸서 $.fn의 프로퍼티로 추가할 수 있다. 현재로서는 이게 곧 jQuery Plugin이라고 생각해도 무방하다.

$.fn.useInvisibleNavigation = function () { // 추가
  let count = 0;
  let threshold = 5;
  let callback = function () { ... };
  const btn = $(this); // 수정
  btn.click(function () { // 수정
    count++;
    if (count == threshold) {
      callback();
    }
  });
}; // 추가

어이쿠! jQuery 플러그인을 하나 만들어 버렸네요!!

  • jQuery 동작 원리상, 여기서의 $(this).useInvisibleNavigation() 메소드가 호출되는/될 대상 엘리먼트의 jQuery 인스턴스다.
    • 그래서 .click()이 존재한다.

위 코드는 어차피 $ === window.jQuery가 호이스팅돼 있어야 작동하는 코드다. 그러므로 IIEF로 감싸두는 것도 가능하다. 랄까 그게 권장된다. 그래서 아래 코드와 같이 "어디서 많이 본" 형태의 코드가 작성된다.

(function($) {
  $.fn.useInvisibleNavigation = function () {
    const btn = $(this);
    let count = 0;
    let threshold = 5;
    let callback = function () { ... };
    btn.click(function () {
      count++;
      if (count == threshold) {
        callback();
      }
    });
  };
})(jQuery);

이제 이것을 invisible-navigation.js로 저장해 두면, 최소한으로나마 진정한 의미에서 재사용이 가능해진다.

<button class="invisble-nav"></button>
<button class="invisble-nav"></button>
<script src="/invisible-navigation.js"></script>
<script>
  $('.invisible-nav').each(function() {
    $(this).useInvisibleNavigation();
  });
</script>

jQuery Plugin 조금 더 들여다보기

옵션화

$.fn.useInvisibleNavigation은 단지 함수일 뿐이므로, 인자를 넘길 수 있다.

(function($) {
  $.fn.useInvisibleNavigation = function (
  	threshold = 5,
    callback = undefined // 이 2개는 원래 변수였음
  ) {
    const btn = $(this);
    let count = 0;
    btn.click(function () {
      count++;
      if (count == threshold && typeof callback == 'function') {
        callback();
      }
    });
  };
})(jQuery);

이제 #gotoadmin 버튼과 #showsuperadmin 버튼 두 가지를 하나의 코드로 분리 운용할 수 있다.

<button id="gotoadmin"></button>
<button id="showsuperadmin"></button>
<div id="superadmin" style="display: none;">슈퍼관리기능</div>
<script src="/invisible-navigation.js"></script>
<script>
  $('#gotoadmin').useInvisibleNavigation(5, function () {
  	window.navigation.href = '/admin';
  });
  $('#showsuperadmin').useInvisibleNavigation(10, function () {
  	$('#superadmin').show();
  });
</script>

그렇다. 사실은 이게 재사용성이다.
'다른 곳에 여러 번 써넣기 좋은 코드'가 아니라, 한 번 써넣은 걸 여러 곳에서 쓸 수 있는 코드 말씀이지.
하지만 jQuery에 길들여진 코드베이스에서는 이런 기초적인 데까지조차 가지 못할 정도로 복사-붙여넣기-조금고치기 의 타성에 젖어 있으므로, 이토록 간단한 플러그인조차도 굉장한 심화 주제로 기능한다.

기왕 옵션화해 놓고 보니 이런 부분도 거슬린다.

  • 현재의 설계에서는 threshold 인자에 부여돼 있는 기본값 5가 별 의미가 없다. 이 인자에 값을 주는 부분을 생략하고 지나갈 수가 없기 때문이다. 그냥 기본값을 쓰라고 넘기고 싶을 땐 어떻게 해야 할까? 인자 순서를 뒤집으면 되겠지만, 그게 정말 해결책일까?
  • 3번째 4번째 인자가 추가되면 그땐 또 모든 .useInvisibleNavigation 사용사례를 뒤져서 전수검사해야 한다. 이보다 더 유지보수성이 좋을 수 없을까?

이런 부분도 해결 가능하다. 옵션들을 객체로 싸서 넘기면 된다.

(function($) {
  $.fn.useInvisibleNavigation = function (options = {}) {
    const btn = $(this);
    const defaultOptions = {
      threshold: 5,
      callback: function () {}
    };
    const option = $.extend(defaultOptions, options);
    let count = 0;
    btn.click(function () {
      count++;
      if (count == option.threshold && typeof option.callback == 'function') {
        option.callback();
      }
    });
  };
})(jQuery);

이것만으로도 훨씬 그럴듯해진다.

$('#gotoadmin').useInvisibleNavigation({
  callback: function () { window.navigation.href = '/admin'; }
});
$('#showsuperadmin').useInvisibleNavigation({
  threshold: 10,
  callback: function () { $('#superadmin').show(); }
});
  • $.extend는 jQuery 내장 메소드로서 객체를 확장해 준다. 짐작하다시피 앞의 것에 뒤의 것을 덧붙여 준다.
    • 3번째 인자로 true를 넘기면 recursive merge를 해주므로, 2단계 이상의 복잡한 defaultOptions 설계도 가능하다.
  • option.callback이 더 이상 undefined가 아니어도 된다.
    • 함수의 인자가 아니기 때문에 더 복잡한 기본 동작을 정의해둘 수도 있다.
    • (나중에 언급할 기회가 있을지 모르겠으나) 이것도 어차피 함수이므로, 같은 스코프 안에 있는 어떤 요소든지 콜백에 넘겨줄 수 있다.

임의의 DOM 생성 조작

마지막으로 한 가지 주제만 더 생각해 보자.

개인적으로는 DOM 안에 <button>이 일일이 준비되어 있어야 한다는 부분이 이상하다. 어차피 투명하고 크기 있는 버튼이다. 이걸 플러그인이 알아서 만들어서 (내가 지정한 위치에) 알아서 삽입하게 바꾸고 싶다.

그렇다면 혹시 .useInvisibleNavigation() 플러그인이 button을 DOM에 삽입하는 것까지도 할 수 있을까?
당연히 된다.
jQuery니까.

// 이름 약간 바꿈
$.fn.addInvisibleNavigation = function (options = {}) {
  const container = $(this); // 상수명에 주목
  const defaultOptions = {
    threshold: 5,
    callback: function () {}
  };
  const option = $.extend(defaultOptions, options);
  let count = 0;
  
  // 여기서부터 주목
  const btn = $('<button></button>');
  btn.css({
    'min-width': '4em',
    'min-height': '4em'
  });
  btn.click(function () {
    count++;
    if (count == option.threshold && typeof option.callback == 'function') {
      option.callback();
    }
  });
  container.append(btn);
};
  • $(HTMLString) 형태로 새 jQuery 인스턴스를 생성하는 것이 가능하다.
  • 그 인스턴스에 미리 이벤트 바인딩을 하는 것이 가능하다.
  • 생성한 인스턴스를 다른 DOM 엘리먼트에 삽입하는 것이 가능하다.
    • 앞서 지정한 모든 처리(.css(), .click() 등)가 모두 살아서 삽입된다.

그리하여 다음과 같은 변경이 정상 작동하게 된다.

<nav>
  <div class="left">
    <a href="/bookings">예약관리</a>
  </div>
  <div class="right" id="has-admin-nav"></div>
</nav>
<script src="/invisible-navigation.js"></script>
<script>
  $('#has-admin-nav').addInvisibleNavigation({
    callback: function () { window.navigation.href = '/admin'; }
  });
</script>

이제 맨 처음의 2개 코드와 이 코드를 비교해 보면, 생각보다 먼 길을 왔음을 알 수 있다.
과연 둘 중 어느 쪽이:

  • 같은 걸 하나 더 추가하거나
  • 기존 기능을 확장 개선하거나
  • 기존 기능의 버그를 고쳐서 적용할 때

더 유리한 전략일까?
여러분의 jQuery 기반 코드를 덜 개판 만들 방법에 대해서 부디 현명한 판단 하길,,

예고

직전의 사례는 솔직히 조금 억지다.
jQuery 플러그인이 임의의 DOM을 만들고 직접 통제/삽입할 수 있다는 사실이 빛을 발하는 사용사례는 다른 데 있다.
그 사례를 소개하며, jQuery 플러그인에서 최소한의 템플리팅을 하는 방법들에 대해 생각해 보고자 한다.

profile
7년차 PHP 개발자입니다.

0개의 댓글