테이블 태그의 데이터를 정렬(table sorting)하는 기능을 구현하면서

자바 스크립트의 기본 문법과 Jquery 사용법을 익힐 수 있도록 정리하였다.

기초 학습을 위해 정리하는 것으로

실제로 사용할 때에는

다양한 기능을 제공하는 오픈 소스를 사용하는 것이 좋다.


데이터를 정렬하는 기능은

그림과 같이 테이블 테그의 헤드(TD, TH)를

마우스로 클릭하면

해당 컬럼(필드)의 데이터를

오름차순이나 내림차순으로 정렬한다.

이해를 돕기 위해

다음과 같이 세부 단계로 나누어서

개발하고 정리한다.


1. 버튼을 누르면 오름차순으로 정렬

2. 정렬할 데이터가 없을 때 까지 반복

3. 정렬 버튼을 누르면 오름차순로 정렬하고, 다시 버튼을 누르면 내림차순으로 정렬, 다시 버튼을 누르면 오름차순로 반복.

4. 3번 예제 단순화

5. JQuery로 변환

6. 버튼 대신 각 테이블의 필드명(첫 row)을 클릭하면 해당 필드가 정렬

7. 필드별 정렬

8. 필드별 정렬 상태 표시

9. 라이브러리화

5단계 부터 9 단계는 나누어서 별도의 페이지에 정리하였다.

각 단계별로 간단한 구현 방법을 먼저 정리하였으니

코드를 보기 전 구현을 시도해 보고

코드를 보는 것이 실력 향상에 조금 더 도움이 될 것이다.


이러한 단계를 진행하기 전에

주어진 데이터를 동적으로 생성하는 것 부터 시작한다.


다음과 같이 주어진 2차원 배열의 데이터를 이용하여

HTML 테이블(table) 태그를 (그림참조)

자바 스크립트(JavaScript)만으로 생성한다.

var data = [
            ['name', 'Java', 'Node', 'Perl'],
            ['Gu', 80, 70, 30],
            ['Brad', 50, 90, 90],
            ['Lee', 70, 70, 70],
            ['Kim', 90, 60, 80]
           ];


먼저, 자바 스크립트에서 HTML 태그들을

동적으로 생성하는 방법을 알아야 한다.

동적으로 생성하는 명령어는 createElement로

함수 파라미터로 HTML 태그를 지정하면 해당 태그가 생성된다.

HTML 태그들은 계층구조로 구성되어 있기 때문에

appendChild나 insertBefore등으로 생성된 태그의 부모를 지정해야 한다.

부모를 지정하지 않으면 웹 페이지에 출력되지 않는다.

보다 상세한 설명은 w3school이나 MDN을 읽어보길 바란다.


주어진 2차원 배열의 데이터를

HTML 테이블(table) 태그로 생성하는 방법은

다음과 같다.

먼저, 테이블을 생성한 부모를 지정한다.

부모 태그는 body로 해도 되고

특정한 div(targetPn)로 해도 된다.

여기서는 HTML div 태그를 찾기 위해

ID가 targetPn인 div 태그를 getElementById를 이용하여 찾았다.


주어진 데이터는 2차원 배열이고

테이블은 행(TR)과 열(TD)로 구성되어 있다.

즉, 2차원 배열에서 각 차원이 행이 되고,

각 차원의 요소가 열이 된다.


따라서 각 차원의 개수만큼 반복해서 [라인 23]

테이블의 자식으로 행(TR)을 생성하고 [라인 24, 25]

각 차원의 요소만큼 반복해서 [라인 26]

행(TR)의 자식으로 열(TD)을 생성한다 [라인 27, 28].

생성 후 열의 값(innerHTML)으로 배열의 값을 지정한다 [라인 29].


웹브라우저의 개발자 도구(DOM 탐색기, Elements)에서

계층형으로 생성된 테이블 태그를 확인할 수 있다.


이제 본격적인 세부 구현을 진행한다.

위 실행 코드에

다음과 같이 버튼 태그를 추가하고

이 버튼을 클릭하면 데이터가 오름차순으로 정렬 되게 한다.

    <button type='button' onclick='sortTable()'>Sort</button>
    <div id='targetPn' style='width:130px'></div>

정렬하는 방법은 다음과 같다.

1. 테이블의 (행의 개수-1) 만큼 반복해서

2. 현재 행(row)과 다음 행(row+1)의 지정된 셀(td) 값을 비교

3. 현재 행의 셀이 다음 행의 셀 값보다 크면

4. 다음 행을 현재 행 앞으로 이동시킨다.



위의 문장은 각각 다음과 같이 해당 라인으로 구현된다.

1. 테이블의 (행의 개수-1) 만큼 반복해서 [라인 42, 43]

2. 현재 행(row)과 다음 행(row)의 지정된 셀(td) 값을 비교 [라인 44, 45]

3. 현재 행의 셀이 다음 행의 셀 값보다 크면 [라인 46]

4. 다음 행을 현재 행 앞으로 이동시킨다 [라인 47].


이상의 코드에서 특징적인 것 중 하나는

getElementsByTagName를 사용한 것이다 [라인 41].

getElementsByTagName는 HTML 태그로 개체를 찾는 명령어로

해당 웹 페이지에 있는 모든 태그를 찾아서 배열로 반환한다.

현재 웹 페이지에는 하나의 테이블만 있기 때문에

0 번째 테이블 태그를 사용하였다 [라인 42].

이외에도 getElementById, getElementsByName, getElementsByClassName등이 있다.


행의 개수 만큼 비교하는 반복 처리는

두번째 행(i=1) 부터

행의 개수(rows.length) 보다 1 작을 때까지 반복한다 [라인 43].

1부터 시작하는 것은

첫번째 필드(i=0)는 필드명으로 정렬하지 않는다.

행의 개수(rows.length)보다 1 적게 반복하는 것은

현재 행과 다음 행을 비교하는데

마지막 행은 다음 행이 없기 때문이다.

따라서, 전체적으로 행의 개수보다 2 적게 반복한다.


table[0].rows를 rows 변수로 지정해서 사용한 것은 [라인 42].

코드를 좀더 보기 좋고 속도(?)를 향상 시키기 위한 것이다.

하위 태그를 사용하기 위해

모든 계층 관계를 점(.)으로 나열하기 보다는

위와 같이 변수로 지정해서 사용하는 것이 좋다.


0 번째 셀(TD) 즉, 사람 이름으로 정렬한다.

제공된 데이터에서 사람 이름이 영문으로 제공되었기 때문에

대소문자 구분을 하지 않기 위해 소문자(toLowerCase)로

변환해서 비교하였다 [라인 46].


비교 결과 현재 행의 이름이 크면

다음 행을 현재 행 앞으로 이동(insertBefore) 시킨다 [라인 46].

insertBefore는 appendChild와 같이 부모를 지정하는 함수이다.

appendChild는 지정된 자식(Node)를

부모의 마지막 자식 뒤에 추가하는 것이고

insertBefore는 지정된 자식(형제) 앞에 삽입하는 함수이다.

         부모. appendChild ( 자식 )

         부모. insertBefore ( 자식, 특정 위치의 형제)

여기서는 현재 행의 부모를 찾아서 행의 이동을 처리했다 [라인 47].

현재 노드의 parentNode 속성으로

부모 노드를 찾을 수 있다.

행의 부모는 테이블 태그이기 때문에

      table[0].insertBefore(rows[i + 1], rows[i])

로 작성해도 된다.

다만, 여기서는 사용하지 않았지만 TBODY를 사용한 경우

이 코드는 오류를 발생시킨다.

다음과 같이 TBODY를 이용해서 작성해야 한다.

     table[0].tbody.insertBefore(rows[i + 1], rows[i])

이렇게 사용할 수도 있고 안 할 수도 있는 경우를 고려해서

parentNode를 이용하는 것이 좋다.


실행 후

버튼을 클릭해서 다음과 같이 정렬이 되는지 확인한다.

데이터를 다음과 같이 수정하여 실행하면

이름이 제대로 정렬되지 않은 것을 확인 할 수 있다.

var data = [
            ['name', 'Java', 'Node', 'Perl'],
            ['Gu', 80, 70, 30],
            ['Kim', 90, 60, 80],
            ['Lee', 70, 70, 70],
            ['Brad', 50, 90, 90]
           ];

4 번째에 있던 Brad가

1 번째가 아닌 3 번째에 있다.

이 버그를 해결하려면,

정렬 버튼을 여러 번 클릭해야 한다.


현재 행과 다음 행만 비교 하기 때문에 발생하는 문제로

여러 번 클릭하면 해결되는 것이 해법이다.

한번 클릭할 때 마다

현재 행과 다음 행을 비교해서 정렬하기 때문에

계속 버튼을 클릭하면

계속 비교를 하게 되어 제대로 정렬이 이루어 진다.

더 이상 정렬할 데이터가 없으면

클릭해도 변화가 없다(비교는 계속 이루어진다).

따라서, 제대로 정렬이 되려면

크기를 비교해서 위치를 바꾸어 주는 것을

정렬할 데이터가 없을 때 까지 반복하면 된다.

    var rows = table[0].rows;
    var chkSort = true;
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var fCell = rows[i].cells[0];
            var sCell = rows[i + 1].cells[0];
            if (fCell.innerHTML.toLowerCase() > sCell.innerHTML.toLowerCase()) {
                rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
                chkSort = true;
            }
        }   
    }

전체 코드

체크 변수(chkSort)를 사용해서

변수의 값이 true일때만 반복 하도록 하였다 (while).

For 문은 데이터 개수(length) 등 반복 범위가 명확 할 때 사용하고

while문은 그렇지 않을 때 사용한다.

while문이 시작되면

체크 변수의 값을 false로 초기 값을 지정한다.

비교문에서 정렬 데이터가 발생하면

체크 변수의 값을 true로 바꾸어

정렬 데이터가 더 있을 수 있으니

처음부터 다시 확인하도록(반복하도록) 한다.


이상의 정렬은 거품 정렬(Bubble sort) 개념을 활용한 것인데

일반적으로 거품 정렬에서는 두번의 for문을 이용한다.

while문이 아니라 이 방법으로 구현해 보길 바란다.


이번에는 정렬 버튼을 누르면 오름차순로 정렬하고,

다시 버튼을 누르면 내림차순으로 정렬,

다시 버튼을 누르면 오름차순로 반복하는 기능을 구현한다.


현재 구현된 코드는 오름차순이다.

오름차순은 큰 값과 작은 값을 비교해서 큰 값을 뒤로 보낸다.

현재 행의 셀 값이 다음 행의 셀 값보다 크면

현재 행을 다음 행 뒤로 보낸다.

다만, 코드로 구현할 때는

다음 행을 현재 행 앞으로 이동 시켰다.


내림차순은 반대로

큰 값과 작은 값을 비교해서 작은 값을 뒤로 보낸다.

현재 행의 셀 값이 다음 행의 셀 값보다 작으면

현재 행을 다음 행 뒤로 보낸다.


버튼을 눌렀을 때,

현재 정렬 상태가 오름차순이면 내림차순으로

내림차순이면 오름차순으로 정렬되도록 한다.

var sortType = 'asc';
function sortTable(){
    var table = document.getElementsByTagName('table');
   
    if (sortType === 'asc') {
        sortType = 'desc';
        sortTableDesc(table[0].rows);
    } else {
        sortType = 'asc';
        sortTableAsc(table[0].rows);
    }
}
function sortTableAsc(rows){
    var chkSort = true;
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var fCell = rows[i].cells[0];
            var sCell = rows[i + 1].cells[0];
            if (fCell.innerHTML.toLowerCase() > sCell.innerHTML.toLowerCase()) {
                rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
                chkSort = true;
            }
        }   
    }
}
function sortTableDesc(rows){
    var chkSort = true;
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var fCell = rows[i].cells[0];
            var sCell = rows[i + 1].cells[0];
            if (fCell.innerHTML.toLowerCase() < sCell.innerHTML.toLowerCase()) {
                rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
                chkSort = true;
            }
        }   
    }
}

전체코드

하나의 버튼으로 오름차순과 내림차순을 구현하려면

전역 변수로 상태 변수(sortType)를 지정해서

변수의 값이 오름차순이면(sortType === 'asc') 내림차순(sortType = 'asc')으로

내림차순이면(sortType === ' desc' 여기에서는 else)

오름차순이(sortType = 'desc') 되도록 하면 된다.

전역 변수(함수 밖)로 선언해야 하는 이유는

함수 내에 선언되어 있으면 지정된 값이 초기화 되기 때문이다.


그리고 각각의 상태에 따라

오름 차순 함수(sortTableAsc)나

내림 차순 함수(sortTableDesc)를 호출한다.


이 두 함수를 자세히 보면

비교연산자(<>)만 다르고 모든 코드가 동일하다.

오름차순은 현재 행의 값이 다음 행의 값 보다 크면(>)

다음 행을 현재 행 앞으로 이동시키고

내림차순은 현재 행의 값이 다음 행의 값 보다 작으면(<)

다음 행을 현재 행 앞으로 이동시킨다.


이상의 코드는

중복 코드도 많고 코드 양도 너무 많다.

이번에는 조금 줄여서 작성해 본다.


비슷한 코드로 구성된 두 함수를 하나로 작성한다.

두 함수의 차이는 비교 연산자뿐이다.

var sortType = 'asc';
function sortTable(){
    sortType = (sortType === 'asc') ? 'desc':'asc';

    var table = document.getElementsByTagName('table');
    var rows = table[0].rows;
    var chkSort = true;
   
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var row = rows[i];
            var fCell = row.cells[0].innerHTML.toLowerCase();
            var sCell = row.nextSibling.cells[0].innerHTML.toLowerCase();
            if ( (sortType === 'asc'  && fCell > sCell) ||
               (sortType === 'desc' && fCell < sCell) ) {
                row.parentNode.insertBefore(row.nextSibling, row);
                chkSort = true;
            }
        }   
    }
}

전체코드

두 개의 정렬 함수를 사용하던 코드를

오름차순만 구현한

이전 코드에 간단한 추가만으로 작성하였다.

먼저, 상태에 따라 변수와 함수를 호출해야 해서

IF문을 사용했지만

함수를 호출하지 않으면 하나의 라인이 되기 때문에

? 문으로 작성할 수 있다. (sortType === 'asc') ? 'desc':'asc';


상태에 따라 정렬하는 것도 코드에서 보이는 것처럼 매우 간단하다.

하나의 IF문에

오름차순이면 > 비교연산자가

내림차순이면 < 비교연산자가 적용되도록하면 된다.


이 예제는 조금 중요한 개념(?)을 설명한다.

실제 개발에서 앞서의 방식으로 작성하는 경우가 아주 많다.

이 경우 실행되기 때문에 개발이 완료 되었다고 할 수 있지만

수정 등의 유지 보수에 많은 시간을 소비할 수 있다.

여유로운 개발을 위해서 뒤의 방식으로 처리하는 습관을 들여야 한다.


마지막으로,

다음 행을 row-1로 찾았던 것을

nextSibling라는 메소드로 작성하였다.

nextSibling은 현재 노드(나)의

다음 번 형제를 찾아주는 기능을 한다.

반대 기능을 하는 previousSibling이 있으며,

이외에도 children등 자식 (노드)관련 메소드가 있다.



참고

거품 정렬에 대한 이해가 부족하다면

https://visualgo.net/en/sorting 사이트를 참고하면 된다.

다양한 정렬 알고리즘을 시각적으로 보여줘서

조금 더 쉽게 이해할 수 있게 해준다.


사이트에 접속하면

다음과 같은 화면이 실행된다.


매우 많은 기능이 제공되니 읽어보길 바라고

[X Esc]를 클릭한다.

페이지 상단에 제공되는 여러 정렬 알고리즘이 나열되어 있다.

이중 가장 앞에 있는 거품정렬(bubble sort)을 선택하고,

좌측 하단의 [Sort]를 클릭하면 나타나는

Go 메뉴를 선택하면

정렬 알고리즘이 진행되는 과정을 볼 수 있다.




+ Recent posts