jQuery / Method / .clone() - 선택한 요소를 복제하는 메서드

jQuery의 .clone()은 선택한 요소와 그 안의 자손·텍스트를 복제한 새 jQuery 객체를 만듭니다. 복제만으로 화면에 추가되지는 않으므로 .appendTo() 같은 삽입 메서드로 원하는 위치에 넣습니다. 기본값은 jQuery 이벤트와 데이터를 복사하지 않으며, 이를 포함할지는 인수로 따로 정합니다.

구조 복제·이동·이벤트 옵션 구분하기

.clone() 공식 문서에서 구조의 깊은 복제는 자식 요소까지 복사한다는 뜻입니다. 이벤트와 .data()까지 모든 내부 객체를 완전히 독립적으로 복사한다는 뜻은 아닙니다. 두 번째 인수 deepWithDataAndEvents도 DOM 자식을 복사할지 정하는 스위치가 아니라 자손의 jQuery 이벤트·데이터를 포함할지 정하는 옵션입니다.

표현 요소와 자손 구조 복제 루트의 이벤트·데이터 자손의 이벤트·데이터
.clone() 복사 복사하지 않음 복사하지 않음
.clone(true, false) 복사 복사 복사하지 않음
.clone(true, true) 복사 복사 복사
.clone(true) 복사 복사 둘째 인수가 첫째 값을 따라 복사

기존 요소를 한 대상에 .appendTo()로 넣으면 원래 자리에서 이동합니다. 원본을 남기려면 먼저 .clone()으로 별도 요소를 만든 뒤 삽입합니다. 복사본은 원본과 다른 DOM(Document Object Model, 문서 객체 모델) 요소이므로 복사본의 제목을 바꿔도 원본 제목은 그대로입니다.

카드의 구조·데이터·클릭을 비교하는 전체 예제

아래 전체 코드를 UTF-8의 clone-demo.html로 저장하고 브라우저에서 엽니다. 코드는 공식 CDN(Content Delivery Network, 콘텐츠 전송 네트워크)의 jQuery 4.0.0을 사용하므로 인터넷 연결이 필요하며 별도 설치는 없습니다. 세 복제 모드, 클릭 결과, 데이터 공유와 초기화를 3.7.1과 4.0.0에서 실행해 확인했습니다.

원본 루트에는 count와 kind라는 jQuery 데이터를 저장하고, 안쪽 Increment 버튼에는 직접 이벤트를 등록했습니다. 복사본의 count와 화면 숫자는 비교를 위해 코드에서 명시적으로 0으로 초기화합니다. 자동으로 복제 카운터가 0이 된다는 뜻은 아닙니다.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Clone structure, data and events</title>
  <style>
    body { font: 16px/1.6 sans-serif; margin: 24px; }
    .card { padding: 14px; margin: 12px 0; border: 1px solid #8aa; }
    .card p { margin: 4px 0; }
    button { margin: 4px; padding: 8px; }
  </style>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
</head>
<body>
  <h1>Clone structure, data and events</h1>
  <div class="card" id="source">
    <p class="label">Original</p>
    <p>Count: <span class="count">0</span></p>
    <p>Data: <span class="data">stored</span></p>
    <button type="button" class="increment">Increment</button>
  </div>
  <button type="button" id="plain">Clone structure</button>
  <button type="button" id="parent">Clone parent data</button>
  <button type="button" id="full">Clone events</button>
  <button type="button" id="reset">Reset</button>
  <div id="copies"></div>
  <p id="result" aria-live="polite"></p>
  <script>
    $(function () {
      const $source = $("#source").data({ count: 0, kind: "stored" });
      let serial = 1;
      function report() {
        $("#result").text(`source=${$source.data("count")} | copies=${$("#copies > .card").length}`);
      }
      $source.find(".increment").on("click", function () {
        const $card = $(this).closest(".card");
        const count = $card.data("count") + 1;
        $card.data("count", count).find(".count").text(count);
        report();
      });
      function copy(withData, deep) {
        const $copy = $source.clone(withData, deep);
        $copy.attr("id", "copy-" + serial);
        $copy.find(".label").text("Copy " + serial++);
        $copy.data("count", 0).find(".count").text(0);
        $copy.find(".data").text($copy.data("kind") || "none");
        $copy.appendTo("#copies");
        report();
      }
      $("#plain").on("click", () => copy(false, false));
      $("#parent").on("click", () => copy(true, false));
      $("#full").on("click", () => copy(true, true));
      $("#reset").on("click", function () {
        $("#copies").empty();
        serial = 1;
        $source.data("count", 0).find(".count").text(0);
        report();
      });
      report();
    });
  </script>
</body>
</html>

처음에는 원본 하나만 있으며 다음 결과가 나옵니다.

source=0 | copies=0
  1. 원본의 Increment를 한 번 눌러 Count를 1로 만듭니다.
  2. Clone structure는 구조를 복사하지만 kind 데이터와 버튼 이벤트는 복사하지 않습니다. Copy 1의 Data는 none이고 Increment를 눌러도 숫자는 0입니다.
  3. Clone parent data는 루트 kind를 복사해 Data가 stored이지만, 자손 버튼의 직접 이벤트는 복사하지 않아 숫자가 늘지 않습니다.
  4. Clone events는 자손 이벤트까지 포함합니다. Copy 3의 Increment를 누르면 그 카드만 1이 되고 원본도 1을 유지합니다.
  5. Reset은 복사본을 모두 제거하고 원본 카운터와 복사 번호를 초기화합니다. 원본 이벤트를 다시 등록하지 않으므로 반복 초기화 후에도 클릭 한 번에 한 번만 증가합니다.

삽입 전에 복제 루트의 ID를 copy-1처럼 고유하게 바꾸며, 내부 항목은 처음부터 클래스로 구분했습니다. ID를 가진 자손이나 label의 for, aria-controls 같은 연결 속성이 있는 구조를 복제한다면 해당 ID와 참조도 함께 새 값으로 맞춰야 합니다.

중첩 객체·배열은 별도로 복사하기

.clone(true)로 복사한 .data() 안의 객체·배열은 원본과 공유될 수 있습니다. 아래 코드는 화면에 삽입하지 않은 임시 요소로 공유와 독립 복사를 비교합니다. 개발자 도구의 Console에 전체 부분 코드를 실행합니다.

const $original = $("<div>").data("options", { tags: ["a"] });
const $shared = $original.clone(true);
$shared.data("options").tags.push("b");
console.log($original.data("options").tags.join(", "));
const $independent = $original.clone(true).data("options", {
  tags: [...$original.data("options").tags]
});
$independent.data("options").tags.push("c");
console.log($original.data("options").tags.join(", "));
console.log($independent.data("options").tags.join(", "));
a, b
a, b
a, b, c

첫 복제본에서 b를 추가하면 같은 options 객체를 참조하는 원본도 a, b가 됩니다. 그 뒤의 독립 복제본은 새 options 객체와 새 tags 배열을 만들어 연결하므로 c를 넣어도 원본은 a, b입니다. 이 코드는 문자열 배열이라는 알려진 데이터 구조를 복사한 것이며, 모든 중첩 객체를 자동으로 깊게 복사하는 일반 해법은 아닙니다.

폼 상태·이벤트 설계·문제 확인하기

폼을 복제할 때는 현재 입력 상태가 모든 종류의 필드에서 똑같이 복사된다고 가정하지 않습니다. 공식 문서는 input의 동적 상태와 textarea·select의 상태를 구분합니다. 실제 필요한 값을 확인하고 선택값·메모 등은 복제 후 .val()로 명시적으로 설정하는 방법을 검토합니다.

새 카드에도 같은 동작이 필요하면 반드시 모든 이벤트를 복사해야 하는 것은 아닙니다. 유지되는 부모에 .on()으로 이벤트를 위임하면 복제 후 추가된 자손도 처리할 수 있습니다. 직접 이벤트 복사와 위임을 함께 사용하면 같은 작업이 두 번 실행될 수 있으므로 필요한 한 방식을 선택하고 결과를 확인합니다.

복사본이 보이지 않으면 삽입 메서드를 호출했는지, 대상 컨테이너가 실제로 있는지 확인합니다. 버튼이 반응하지 않으면 이벤트가 루트와 자손 중 어디에 등록되었는지, 두 인수가 무엇인지 확인합니다. 선택자가 비면 복제도 빈 집합이며, 중복 ID가 있으면 ID 선택이나 폼 레이블이 잘못 연결될 수 있습니다. Network의 jQuery 로딩과 Console 오류도 함께 점검합니다.