jQuery로 table 제어하기

예제 기준 · jQuery : v3.7.1

table에서 행을 추가·삭제하거나 선택한 행의 값을 다룰 때는 작업할 행과 셀을 정확히 찾는 것이 먼저입니다. 행 선택, checkbox 선택, 셀 읽기·수정과 함께 새로 추가된 행의 이벤트 처리까지 살펴보겠습니다.

예제의 표는 ID·이름·조작 열로 구성됩니다. 본문 영역은 tbody#rows, 각 행의 식별자는 data-id, 이름 셀은 .name-cell입니다. 초기 데이터는 ID 1~3과 Test 1~3입니다. 각 절의 코드는 해당 예제에서 따로 사용하며 HTML 요소가 만들어진 뒤 실행합니다. jQuery 코드는 jQuery가 로드된 화면을 기준으로 합니다.

행 추가하기

tr과 td를 만든 뒤 tbody에 추가합니다. 아래 코드는 ‘행 추가’ 버튼을 누르면 ID를 하나씩 늘려 이름과 확인 버튼을 가진 행을 만듭니다.

jQuery

function createRow(number) {
    const row = $('<tr>');
    row.attr('data-id', String(number));
    const idCell = $('<td>');
    idCell.text(number);
    const nameCell = $('<td>', { class: 'name-cell' });
    nameCell.text('Test ' + number);
    const actionCell = $('<td>');
    const button = $('<button>', {
        type: 'button', class: 'row-action', text: '확인 ' + number
    });
    actionCell.append(button);
    row.append(idCell);
    row.append(nameCell);
    row.append(actionCell);
    return row;
}

let serial = 3;
$('#add').on('click', function () {
    serial += 1;
    $('#rows').append(createRow(serial));
});

JavaScript

function createRow(number) {
    const row = document.createElement('tr');
    row.dataset.id = String(number);
    const idCell = document.createElement('td');
    idCell.textContent = String(number);
    const nameCell = document.createElement('td');
    nameCell.className = 'name-cell';
    nameCell.textContent = 'Test ' + number;
    const actionCell = document.createElement('td');
    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'row-action';
    button.textContent = '확인 ' + number;
    actionCell.append(button);
    row.append(idCell, nameCell, actionCell);
    return row;
}

let serial = 3;
document.querySelector('#add').addEventListener('click', function () {
    serial += 1;
    document.querySelector('#rows').append(createRow(serial));
});

초기 행이 3개이므로 첫 추가 행은 ID 4, 이름은 Test 4입니다. 번호는 현재 행 개수로 계산하지 않고 별도로 증가시킵니다. 행을 삭제해도 이미 사용한 번호를 다시 만들지 않기 위해서입니다. 실제 데이터가 다른 화면에서는 시작 번호도 기존 ID에 맞춰 정해야 합니다.

문자열 데이터는 .text() 또는 textContent로 넣으면 HTML로 해석되지 않습니다. 위 코드는 행 생성 부분이며, 아래 예제는 추가한 ID와 전체 행 수도 표시합니다. 확인 버튼의 이벤트 연결은 마지막 절에서 다룹니다.

클릭한 버튼이 속한 행 삭제하기

삭제 버튼에서 closest('tr')로 가장 가까운 행을 찾습니다. 몇 번째 행인지 계산할 필요 없이 클릭한 버튼의 행만 제거할 수 있습니다.

jQuery

$('#rows').on('click', '.remove', function () {
    const row = $(this).closest('tr');
    const id = row.attr('data-id');
    row.remove();
    console.log('삭제한 ID:', id);
});

JavaScript

const rows = document.querySelector('#rows');
rows.addEventListener('click', function (event) {
    if (!(event.target instanceof Element)) {
        return;
    }
    const button = event.target.closest('.remove');
    if (button === null || !rows.contains(button)) {
        return;
    }
    const row = button.closest('tr');
    if (row === null || row.parentElement !== rows) {
        return;
    }
    const id = row.dataset.id;
    row.remove();
    console.log('삭제한 ID:', id);
});

jQuery의 위임 핸들러에서 this는 .remove에 맞는 버튼입니다. JavaScript의 event.target은 버튼 안쪽 요소일 수도 있어 먼저 .remove 버튼을 찾습니다. 이어서 #rows의 직접 자식 행인지 확인하므로 중첩된 표의 행을 잘못 처리하지 않습니다.

‘행 2 삭제’를 누르면 ID 2만 없어지고 나머지 ID는 유지됩니다. 아래 예제는 삭제한 ID와 남은 행 수를 표시합니다. 이 삭제는 화면의 DOM을 제거하는 동작입니다.

하나의 행 선택하기

선택 상태를 is-selected class로 관리합니다. 새 행을 선택하기 전에 기존 선택을 모두 해제하면 한 번에 하나의 행만 선택됩니다. 선택 버튼의 aria-pressed도 함께 갱신합니다.

jQuery

function selectRow(row) {
    $('#rows tr').removeClass('is-selected');
    $('#rows .choose').attr('aria-pressed', 'false');
    row.addClass('is-selected');
    row.find('.choose').attr('aria-pressed', 'true');
}

$('#rows').on('click', 'tr', function () {
    selectRow($(this));
});

JavaScript

const rows = document.querySelector('#rows');

function selectRow(row) {
    const allRows = rows.querySelectorAll('tr');
    const buttons = rows.querySelectorAll('.choose');
    for (let i = 0; i < allRows.length; i++) {
        allRows[i].classList.remove('is-selected');
    }
    for (let i = 0; i < buttons.length; i++) {
        buttons[i].setAttribute('aria-pressed', 'false');
    }
    row.classList.add('is-selected');
    row.querySelector('.choose').setAttribute('aria-pressed', 'true');
}

rows.addEventListener('click', function (event) {
    if (!(event.target instanceof Element)) {
        return;
    }
    const row = event.target.closest('tr');
    if (row === null || row.parentElement !== rows) {
        return;
    }
    selectRow(row);
});

행이나 ‘행 1 선택’ 같은 버튼을 누르면 같은 선택 처리가 실행됩니다. 키보드로는 선택 버튼을 사용합니다. class는 상태를 나타내며 강조 색상은 .is-selected에 연결한 CSS로 표시합니다. class를 추가하는 것만으로 배경색이 생기는 것은 아닙니다.

이 코드는 다시 누르면 해제되는 토글이 아니라 하나를 선택하는 방식입니다. 아래 예제는 선택한 행의 ID도 표시합니다. 셀 읽기·수정 예제 역시 같은 선택 코드를 사용합니다.

checkbox로 여러 행 선택하기

여러 행을 선택할 때는 각 행의 .row-check checkbox를 사용합니다. value는 행 ID와 같은 1, 2, 3이며 문자열로 읽힙니다. ‘전체 선택’은 #all, ‘전체 해제’는 #none, ‘선택 행 확인’은 #read 버튼입니다.

jQuery

function updateState() {
    const selected = $('#rows .row-check:checked');
    const ids = [];
    for (let i = 0; i < selected.length; i++) {
        ids.push($(selected[i]).val());
    }
    $('#result').text('선택한 행 ID: ' + JSON.stringify(ids));
}

$('#all').on('click', function () {
    $('#rows .row-check').prop('checked', true);
    updateState();
});
$('#none').on('click', function () {
    $('#rows .row-check').prop('checked', false);
    updateState();
});
$('#read').on('click', updateState);
$('#rows .row-check').on('change', updateState);
updateState();

JavaScript

function updateState() {
    const selected = document.querySelectorAll('#rows .row-check:checked');
    const ids = [];
    for (let i = 0; i < selected.length; i++) {
        ids.push(selected[i].value);
    }
    document.querySelector('#result').textContent =
        '선택한 행 ID: ' + JSON.stringify(ids);
}

function setAllChecked(checked) {
    const boxes = document.querySelectorAll('#rows .row-check');
    for (let i = 0; i < boxes.length; i++) {
        boxes[i].checked = checked;
    }
    updateState();
}

document.querySelector('#all').addEventListener('click', function () {
    setAllChecked(true);
});
document.querySelector('#none').addEventListener('click', function () {
    setAllChecked(false);
});
document.querySelector('#read').addEventListener('click', updateState);
const boxes = document.querySelectorAll('#rows .row-check');
for (let i = 0; i < boxes.length; i++) {
    boxes[i].addEventListener('change', updateState);
}
updateState();

행 1과 행 3을 체크하면 ["1","3"], 전체 해제하면 []가 표시됩니다. 개별 체크를 바꾸면 change에서 즉시 결과를 갱신합니다. 코드로 checked를 바꿀 때는 change가 자동 발생하지 않으므로 전체 선택·해제 후 updateState()를 직접 호출합니다.

이 예제는 전체 선택용 checkbox 없이 두 버튼을 사용하므로 일부 선택을 표시하는 indeterminate 상태는 없습니다. 선택 여부는 is-selected class와 별개인 각 checkbox의 checked로 판단합니다. 체크한 행의 셀도 필요하다면 해당 checkbox에서 closest('tr')로 행을 찾아 읽으면 됩니다.

현재 있는 세 checkbox에는 이벤트를 직접 연결했습니다. 나중에 checkbox 행을 추가하는 화면이라면 마지막 절의 위임 방식으로 change를 연결할 수 있습니다.

선택한 행의 특정 셀 값 읽기

앞의 단일 행 선택 코드를 연결한 상태에서 tr.is-selected를 찾고 이름 셀의 텍스트를 읽습니다. 아무 행도 선택하지 않았다면 안내하고 처리를 끝냅니다.

jQuery

$('#read').on('click', function () {
    const row = $('#rows tr.is-selected');
    if (row.length === 0) {
        $('#result').text('먼저 행을 선택하세요.');
        return;
    }
    const cell = row.find('.name-cell');
    $('#result').text('ID ' + row.attr('data-id') + '의 이름: ' + cell.text());
});

JavaScript

document.querySelector('#read').addEventListener('click', function () {
    const row = document.querySelector('#rows tr.is-selected');
    const result = document.querySelector('#result');
    if (row === null) {
        result.textContent = '먼저 행을 선택하세요.';
        return;
    }
    const cell = row.querySelector('.name-cell');
    result.textContent = 'ID ' + row.dataset.id + '의 이름: ' + cell.textContent;
});

‘행 2 선택’ 후 ‘이름 셀 읽기’를 누르면 Test 2를 읽습니다. HTML에 줄바꿈과 들여쓰기가 있으면 읽은 텍스트에도 공백이 포함될 수 있습니다. 표시용 값의 양끝 공백을 제거해야 한다면 읽은 문자열에 trim()을 적용합니다.

두 번째 셀을 row.find('td').eq(1) 또는 DOM 행의 row.cells[1]로 찾을 수도 있습니다. 인덱스는 0부터 시작합니다. 하지만 앞에 열을 추가하거나 열 순서를 바꾸면 다른 셀을 읽게 됩니다. colspan·rowspan이 있으면 셀 배열의 인덱스와 화면상의 열 위치도 다를 수 있으므로, 이름처럼 역할이 정해진 셀은 .name-cell 같은 선택자로 찾는 편이 수정에 대응하기 쉽습니다.

일반 셀의 텍스트에는 .text()/textContent를 사용합니다. 셀 안의 input에 입력한 현재 값이 필요하다면 input을 찾아 .val()/value로 읽어야 합니다.

선택한 행의 특정 셀 값 변경하기

단일 행 선택 코드를 연결하고, ‘새 이름’ 입력란 #newName의 값을 ‘이름 변경’ 버튼 #update로 반영합니다. 초기 입력값은 Updated입니다.

jQuery

$('#update').on('click', function () {
    const row = $('#rows tr.is-selected');
    const name = $('#newName').val().trim();
    if (row.length === 0 || name === '') {
        $('#result').text('행을 선택하고 새 이름을 입력하세요.');
        return;
    }
    const cell = row.find('.name-cell');
    const before = cell.text();
    cell.text(name);
    $('#result').text('변경 전: ' + before + '\n변경 후: ' + name);
});

JavaScript

document.querySelector('#update').addEventListener('click', function () {
    const row = document.querySelector('#rows tr.is-selected');
    const name = document.querySelector('#newName').value.trim();
    const result = document.querySelector('#result');
    if (row === null || name === '') {
        result.textContent = '행을 선택하고 새 이름을 입력하세요.';
        return;
    }
    const cell = row.querySelector('.name-cell');
    const before = cell.textContent;
    cell.textContent = name;
    result.textContent = '변경 전: ' + before + '\n변경 후: ' + name;
});

행을 고르지 않았거나 입력이 공백뿐이면 변경하지 않습니다. 선택한 행의 이름만 바뀌며 data-id는 그대로 유지됩니다. 입력에 HTML 모양의 문자열이 있어도 텍스트로 표시합니다.

.text(name)과 textContent 대입은 셀 내부 내용을 교체합니다. 버튼이나 input까지 들어 있는 셀 전체에 적용하면 그 요소들도 사라지므로 이름 전용 셀만 대상으로 삼습니다.

동적으로 추가된 행의 이벤트 처리

페이지가 처음 열릴 때 버튼에 이벤트를 직접 연결하면 그 시점에 존재하는 버튼만 대상이 됩니다. 이후 만든 새 버튼에는 같은 이벤트가 자동으로 연결되지 않습니다.

jQuery: 직접 등록

$('#rows .row-action').on('click', function () {
    const row = $(this).closest('tr');
    console.log('ID:', row.attr('data-id'));
});

JavaScript: 직접 등록

const buttons = document.querySelectorAll('#rows .row-action');
for (let i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener('click', function (event) {
        const row = event.currentTarget.closest('tr');
        console.log('ID:', row.dataset.id);
    });
}

행을 추가할 때마다 새 버튼에 직접 연결할 수도 있지만, 계속 유지되는 tbody에 이벤트를 한 번 위임하면 추가한 버튼도 처리할 수 있습니다. 버튼의 click이 부모로 전파되는 버블링을 이용합니다. 다음 코드는 직접 등록 방식 대신 사용합니다.

jQuery: 이벤트 위임

$('#rows').on('click', '.row-action', function () {
    const row = $(this).closest('tr');
    const name = row.find('.name-cell').text();
    $('#result').text('이벤트 위임: ID=' + row.attr('data-id') + ' | ' + name);
});

JavaScript: 이벤트 위임

const rows = document.querySelector('#rows');
rows.addEventListener('click', function (event) {
    if (!(event.target instanceof Element)) {
        return;
    }
    const button = event.target.closest('.row-action');
    if (button === null || !rows.contains(button)) {
        return;
    }
    const row = button.closest('tr');
    if (row === null || row.parentElement !== rows) {
        return;
    }
    const name = row.querySelector('.name-cell').textContent;
    document.querySelector('#result').textContent =
        '이벤트 위임: ID=' + row.dataset.id + ' | ' + name;
});

jQuery의 .on(event, selector, handler)는 부모에서 이벤트를 받고 selector에 맞는 자식의 동작을 처리합니다. JavaScript에서는 부모에 addEventListener()를 연결하고 target에서 해당 버튼을 직접 찾습니다. 이때 currentTarget은 버튼이 아닌 이벤트를 등록한 tbody입니다.

행 추가에는 첫 절의 createRow()와 #add 클릭 코드를 함께 사용합니다. 아래에서 ‘행 추가’를 누른 뒤 ‘확인 4’를 누르면 새 행의 ID와 이름을 읽습니다. 아래 예제는 위임 방식을 사용하며 직접 등록 방식으로 전환하는 버튼은 없습니다.

이벤트를 위임한 #rows 자체를 새 요소로 교체하면 연결도 다시 해야 합니다. 또한 자식에서 stopPropagation()으로 click 전파를 막으면 부모의 핸들러까지 도달하지 않습니다. 가능한 한 대상과 가까우면서 계속 유지되는 부모를 선택합니다.

jQuery와 JavaScript 주요 문법 비교

아래의 $row·$cell·$box는 jQuery 객체, row·cell·box는 DOM 요소입니다.

기능 jQuery JavaScript
소속 행 찾기 $(button).closest('tr') button.closest('tr')
행 추가 $('#rows').append($row) rows.append(row)
행 삭제 $row.remove() row.remove()
선택 class 추가 $row.addClass('is-selected') row.classList.add('is-selected')
선택 class 제거 $row.removeClass('is-selected') row.classList.remove('is-selected')
이름 셀 찾기 $row.find('.name-cell') row.querySelector('.name-cell')
셀 텍스트 읽기 $cell.text() cell.textContent
셀 텍스트 변경 $cell.text(name) cell.textContent = name
checkbox 선택 $box.prop('checked', true) box.checked = true
이벤트 위임 .on('click', selector, handler) addEventListener()와 event.target.closest()

행은 closest('tr')로 찾고, 선택 상태는 class 또는 checkbox의 checked로 구분합니다. 셀은 열 번호보다 역할을 나타내는 선택자로 찾으면 열 구조가 바뀔 때 수정 범위를 줄일 수 있습니다. 동적으로 행을 추가하는 화면에서는 유지되는 tbody에 이벤트를 위임하면 새 행마다 핸들러를 다시 등록할 필요가 없습니다.

참고자료