마지막으로 시계 바늘(Clock Hands)을 구현한다.

전체 코드는 여기에서 확인할 수 있고

주요 부문에 대한 자바스크립트 코드는 다음과 같다.


현재 시각(Date)을 구한 뒤

시간, 분, 초로 나누어서

변수(hour, minute, second)에 저장한다.

코드는 시간, 분, 초의 순서로 작성했지만

코드가 초, 분, 시간 순서로 복잡해 지기 때문에

쉬운것 부터 정리한다.


먼저 초(second)는

최대값이 60으로

1초당 360도를 60 등분한 만큼씩(1초=6도) 이동하면 된다.

즉, X * 360 / 60 이 공식이 되고

라디안으로 바꾸면

X * (PI * 2) / 60 = X * PI / 30 이 된다.

즉 초당 X * PI / 30 씩 각도를 바꾸어(rotate) 주면 된다 [라인 14].

정리하면, 1초에 6도씩 움직인다는 것이다.

(2초면 12도, 3초...)


다음으로 분(minute)도

최대값이 60으로

1초당 360도를 60 등분한 만큼씩 이동하기 때문에

X * PI / 30 로 계산해 주면 된다 [라인 11].

다만, 1분은 60초로 구성되고

초가 흐른 만큼 분을 표시할 각도에 추가해 주어야 한다.

이 크기는 아주 적지만 포함 할수록 정확한 각도가 나오게 된다.

(분에서는 처리하지 않아도 구별하기 힘들지만 시간 처리에서는 중요)

앞서의 초에 대한 공식에서

전체 각도 360 (PI *2)을 60으로 나누었지만

분 단위 처리에서는 1분에 대한 초를 처리하는 것이라

1분이 가는 각도(6도)를 1/60초씩 증가해야 한다.

따라서, 좀더 정확한 분의 각도를 계산하기 위해서는

다음과 같은 공식이 추가되어야 한다.

1분이 가는 각도 * S * 1 / 60

= (1 * PI / 30) * (S * 1 / 60)

= S * PI / (30 * 60)


즉, 다음과 같이

분의 각도는 분 + 초의 각도가 된다 [라인 11].

(minute*PI/30) + (second*PI/(30*60)


마지막으로 시간은(hour)은

최대값이 12로

1 시간당 360(PI*2)도를 12 등분한 만큼씩 이동하기 때문에

H * PI / 6 (1시간=30도)으로 계산해 주면 된다 [라인 8].

다만, 시간도 보다 정확한 각도를 계산하기 위해

현재의 분과 초에 대한 각도를 추가해 주면 된다.

시간에 대한 분은

1 시간이 가는 각도 * M * 1 / 60

= (1 * PI / 6) * (M * 1 / 60)

= M * PI / (6 * 60)


초 계산이 앞서 분에 대한 초와 조금 차이가 있다.

여기서는 시간에 대한 분에 대한 초로 계산해야 한다.

즉, 1시간에 대한 분의 각도를 1/60으로 나누어

주어진 초만큼 증가 시켜야 한다.

즉,

1 시간에 대한 1분이 가는 각도 * S * 1 / 60

= M(=1) * PI / (6 * 60) * (S * 1 / 60)

= S * PI / ( 6 * 60 * 60)


정리하면

1시간이 가는 각도 * 현재 시간

+ 1시간에 가는 각도 / 60 * 현재 분

+ 1시간에 가는 각도에서 1분이 가는 각도 / 60 * 현재 초

로 계산하여 [라인 8]

=(hour*Math.PI/6)+(minute*Math.PI/(6*60))+(second*Math.PI/(360*60))

로 프로그램 코드를 작성하였다.


이렇게 계산된 각도에 따라서

drawHand 함수를 호출해서 시각을 표시한다.

drawHand 함수는 지정된 각도(pos)에

시간과 분은 굵게 (radius*0.07)

초는 가늘게 (radius*0.02) 선을 긋는다 (width).

선의 길이(length)는

시간(radius*0.5), 분 (radius*0.8), 초 (radius*0.9)

순으로 길게 그려준다.




앞서의 w3schools의 예제는

rotate와 translate를 이용하여

좌표의 기준 축을 이동시켜서

각각의 위치에 시간을 표시했다.


여기서는 간단한 삼각함수를 이용하여 구현하는 방법을 정리한다.

먼저, 시계를 구현하기 위해 삼각함수가 필요한 이유는

다음 그림에서 첫 번째 그림을 참고하면 된다.

시간을 출력하기 위해서

각 시간의 X, Y 좌표 값을 계산한다.

예로 1시의 좌표는 그림에서 보듯이

시계 중앙에서 시작된 직삼각형을 이용할 수 있다.


먼저, X 좌표는 두 번째 그림의 b길이에 해당한다.

(b의 시작점은 0 이고 종료점이 길이 이기 때문에...)

a 값은 원의 반지름,

즉 앞서 예제에서는 radius가 된다.

각도는 1 / 12 * 360 이므로 30도로 계산된다.

즉, 한변과 각도(끼인각)을 이용하면 b를 계산할 수 있고

이때 사용하는 것이 삼각함수 cos 이고

공식은 다음과 같다.

cos 30 = b / a

=> b = cos 30 * a

다만, 라디안으로 구현해야 하므로

=> b = cos (30 / 180) * a


다음으로 Y좌표는

그림에서 보듯이 c 값을 구해야 한다.

각도와 a의 값을 알고 있으니

삼각함수 sin을 이용해서 계산한다.

sin 30 = c / a

=> c = sin 30 * a

=> c = sin (30 / 180) * a


정리하면 X, Y 좌표는

X = cos (30 / 180) * a

Y = sin (30 / 180) * a

으로 간단하게 구할 수 있다.


그런데 다음 그림과 같이 문제가 있다.

첫번째 그림과 같이 Canvas에서는 시작하는 각도는

12시 부분이 기준으로 0 (360)도가 된다.

삼각함수는 두 번째 그림과 같이

3시 부분이 기준으로 0 (360)도가 된다.

이 내용을 다시 다음 표로 정리했다.

시간

시간에 대한 각도

실제 각도


30 
-60 


60 
-30 

90 


120 
30 

150 
60 

180 
90 

210 
120 

240 
150 

270 
180 
10 
300 
210 
11 
330 
240 
12 
360 
270 

여기서 공식이 나오게 된다.

각 시간의 실제 각도 (시간 * 30)는

시간에 계산된 각도에서 90을 빼준 값(-90)이다.

즉, 끼인각은

(시간 * 30) - 90

이라는 공식으로 계산하게 된다.

이 공식을 앞서의 좌표와 합치면 다음과 같다.

X = cos (((시간 * 30) - 90) / 180) * a

Y = sin (((시간 * 30) - 90) / 180) * a


이것을 자바스크립트로 작성하면

다음 코드의 8과 9라인이 된다.

공식에서 사용된 변수 a는 반지름을 의미하므로

radius값을 이용한 pos(시계보다 작은값 부여) 된다.

w3schools 예제에서는 fillText 함수의 좌표 값이 0, 0 이었지만

여기서는 계산된 좌표 값이 부여 되었다.

w3schools는 기준축을 이동시켰고

여기서는 기준축을 시계 중앙에 고정시키고

출력할 좌표 값을 변경하는 방식을 사용한 것이다.




마지막 처리로

시간, 분, 초에 대한 시계 바늘을

삼각함수를 이용하여 자바 스크립트로 구현한다.


원리는 앞서 구현한 시간을 표현하는 것과

동일한 방식으로 계산하기 때문에

다음 내용을 읽기 보다는

직접 구현해 보는 것이 실력향상에 도움이 될 것이다.


여기서 정리할 것은

모두 앞에서 정리되었기 때문에,

여기서는 간단하게 몇 가지만 정리한다.


w3schools 예제에서 현재 시각(Date)을 구한 뒤

시간, 분, 초로 나누어서

변수(hour, minute, second)에 저장했다.

코드는 시간, 분, 초의 순서로 작성했지만

코드가 초, 분, 시간 순서로 복잡해 지기 때문에

쉬운것 부터 정리한다.

clock.html

먼저, 초를 삼각함수를 이용하여 계산하는 것은

시간과 동일하다.

X = cos (((시간 * 30) - 90) / 180) * a
Y = sin (((시간 * 30) - 90) / 180) * a

시간에서 사용된 위 코드와

21라인과 22라인의 계산 공식을 비교해 보면 동일한 것을 알 수 있다.

30 과 6 이라는 값만 차이가 있다.

시간은 360도를 12로 나누어서 시간당 30도의 값을 가진 것이고

초는 360도를 60으로 나누어서 초당 6도의 값을 가진 것 뿐이다.


다음으로 분도 같은 공식으로 계산된다.

다만, 미세하지만 초가 증가한 만큼 (6/60*second)

분의 각도를 더해 줘야 한다 [15, 16라인].

초가 증가한 만큼 분의 각도를 더해만 주면 되기 때문에

90을 빼지 않는다.

특히, 분에 대한 초의 각도는

360도를 60초 동안 가는 것으로 계산하는 것이 아니고

1분이 가는 각도 6 도를 60으로 나누어서 가기 때문에

증가한 값은 아주 적다.


마지막으로 시간도 앞서 계산한 공식과

같은 원리에 의해서 계산된다 [9, 10 라인].

분침에 초의 값을 더하는 것은 의미가 적지만

시간은 실행 결과가 조금 다를 수 있다.

따라서, 시간의 각도에 분과 초의 각도를 제대로 계산해야 한다.

예로 5시 30분인데 시침이 5시에 있다면 버그가 될 것이다.

시침은 5시와 6시 사이에 있어야 한다.

시간에 대한 분의 각도 계산은 앞서와 동일한 방식으로

1시간에 증가하는 각도 30도를

60으로 나누어서 현재 분의 값을 곱해주면 된다.

= 30 / 60 * 분


실제로 시계 바늘을 그리는 drawHand 함수에도 조금 차이가 있다.

w3schools 예제에서는

각도(pos), 바늘 길이(length), 바늘 굵기(width)가 파라메타로 필요했다.


작성된 코드의 26라인을 보면

각도와 길이 대신에 x, y 좌표로 바뀌었다.

w3schools 예제에서는 어떤 각도에 얼마큼의 길이로 그려야 했고

삼각함수에서는

중앙에서 계산된 좌표로 선을 그리면 되기 때문이다.


지금까지 2가지 방식으로

같은 기능을 구현하는 방식을 정리하였다.

어떤 방식이 더 좋다고 단정할 수 없고

개인의 선택 문제로 자신에게 알맞는 방식을 찾길 바란다.


앞서의 예제는

Canvas에서 두가지 방법으로 구현해 보았다.

여기에서는 Canvas 대신에

SVG (Scalable Vector Graphics)를 이용하여 구현한다.

SVG 장점이나 문법은 알고 있다는 전제로 정리한다.


SVG로 구현하기 전에

삼각함수로 구현한 코드

다음 코드와 같이 변경 작업을 진행해야 한다.

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var center = canvas.height / 2;
//ctx.translate(radius, radius);
var radius = center * 0.90
drawClock();

function drawClock() {
  drawFace(ctx, radius);
  drawNumbers(ctx, radius);
  drawTime(ctx, radius);
}

function drawFace(ctx, radius) {
  var grad;
  ctx.beginPath();
  ctx.arc(center, center, radius, 0, 2*Math.PI);
  ctx.fillStyle = 'white';
  ctx.fill();
  grad = ctx.createRadialGradient(center,center,radius*0.95, center,center,radius*1.05);
  grad.addColorStop(0, '#333');
  grad.addColorStop(0.5, 'white');
  grad.addColorStop(1, '#333');
  ctx.strokeStyle = grad;
  ctx.lineWidth = radius*0.1;
  ctx.stroke();
  ctx.beginPath();
  ctx.arc(center, center, radius*0.1, 0, 2*Math.PI);
  ctx.fillStyle = '#333';
  ctx.fill();
}

function drawNumbers(ctx, radius) {
    ctx.font = radius*0.15 + "px arial";
    ctx.textBaseline="middle";
    ctx.textAlign="center";
 
    var pos = radius*0.85;
    for (var num = 1; num < 13; num++) {
        var x = pos * Math.cos(Math.PI* ((30 * num)-90)/ 180);
        var y = pos * Math.sin(Math.PI* ((30 * num)-90)/ 180);

        ctx.fillText(num.toString(), x+center, y+center);
    }
}

function drawTime(ctx, radius){
    var now = new Date();
    var hour = now.getHours();
    var minute = now.getMinutes();
    var second = now.getSeconds();
    var pos = radius*0.5;
   
    //hour
    x = pos* Math.cos(Math.PI* ((hour*30)- 90 + 30/60*minute + 30/60/60*second) / 180);
    y = pos* Math.sin(Math.PI* ((hour*30)- 90 + 30/60*minute + 30/60/60*second) / 180);
    drawHand(ctx, x, y, radius*0.07);
   
    //minute
    pos = radius*0.8;
    x = pos * Math.cos(Math.PI* ((minute * 6)- 90 + 6/60*second)/ 180);
    y = pos * Math.sin(Math.PI* ((minute * 6)- 90 + 6/60*second)/ 180);
    drawHand(ctx, x, y, radius*0.07);
   
    // second
    pos = radius*0.9;
    x = pos * Math.cos(Math.PI* ((second * 6)- 90)/ 180);
    y = pos * Math.sin(Math.PI* ((second * 6)- 90)/ 180);
    drawHand(ctx, x, y, radius*0.02);
}

function drawHand(ctx, x, y, width) {
    ctx.beginPath();
    ctx.lineWidth = width;
    ctx.lineCap = "round";
    ctx.moveTo(center,center);
    ctx.lineTo(x+center, y+center);
    ctx.stroke();
}

clock1.html

삼각함수로 구현한 코드와 차이는

translate(radius, radius)로 기준점을 화면(canvas) 중앙으로 하지 않고

원래 기준점(화면의 좌측 상단)을 유지하는 것이다.

이전 코드에서는 화면 중앙을 기준으로 했기 때문에

대부분의 좌표가 0 이었다.

좌측 상단이 0 이 되면,

도형을 그릴 위치를 지정해야 한다.


예로, 시계 외곽선을 그리기 위해

arc를 호출한다.

화면 중앙을 기준으로 할 경우

0,0 에서 반지름 만큼 그리도록 호출하면 된다 (arc(0,0,radious)).

좌측 상단을 기준으로 하면

화면 중앙에서 반지름 만큼 그리도록 호출하면 된다 (arc(center,center,radious)).

따라서 위 코드에서 보는 것 처럼 center 변수를 추가하였다.


이렇게 작성하는 것이 

책이나 인터넷 자료에서 가장 흔하게 보는 일반적인 코드 작성법이고

SVG를 좀더 쉽게 사용할 수 있어서 코드를 수정하였다.



SVG로 구현하기 전에

간단한 사용법을 익히고 진행한다.

여기에서 정리한 사용법에 맞추어

몇 가지 코드만 바꾸어 주면

Canvas를 SVG로 간단하게 구현할 수 있다.

clock2.html

위 코드 1라인에서

Canvas대신 SVG 태그를 생성하여 사용한다.

이 SVG태그를 getElementById로 찾아서

clock 변수에 담아서 자바 스크립트(JS)로 제어한다 [라인2].


Canvas에서는 도형들을 그렸지만

SVG에서는 도형이라는 HTML 태그를 생성한다.

따라서

시계의 틀(face)을 그리기 위해 큰 원(Circle)을

하나 생성하였다 [라인 15].

JS에서는 동적으로 태그를 생성할 때 createElement를 사용하지만

SVG는 도형을 동적으로 생성할 때 createElementNS를 사용한다.

생성 후에 appendChild를 이용하여 부모를 지정해 준다.

부모를 지정하지 않으면 웹 페이지에 표시 되지 않는다 [라인 16].

마지막으로 원의 속성(setAttribute)들을 지정한다.


다음 코드는 Canvas에서 사용한 코드로

시계 테두리에 그라데이션 준 것을

SVG에서는 선(원)으로 바꾸어 작성하였다.

  ctx.beginPath();
  ctx.arc(center, center, radius, 0, 2*Math.PI);
  ctx.fillStyle = 'white';
  ctx.fill();
  grad = ctx.createRadialGradient(center,center,radius*0.95, center,center,radius*1.05);
  grad.addColorStop(0, '#333');
  grad.addColorStop(0.5, 'white');
  grad.addColorStop(1, '#333');
  ctx.strokeStyle = grad;
  ctx.lineWidth = radius*0.1;
  ctx.stroke();


Canvas에서는 함수의 파라미터나

Canvas 속성을 이용하여

그리고자 하는 도형의 다양한 속성(색, 크기 등)을 지정하였다.

SVG에서는 속성으로 지정한 차이를 볼 수 있다 [라인 17~].


다음 그림은 Canvas를 사용했을때의 DOM 탐색기의 모습이다.

시계가 작동되고 있지만

Canvas 태그 하위에 아무것도 없는 것을 볼 수 있다.


다음 그림은 SVG를 사용하여

이상의 예제를 실행했을 때의 DOM 탐색기의 모습이다.

SVG 태그 하위에 원(circle) 태그가 생성된 것을 볼 수 있다.

즉, Canvas는

Canvas에 다양한 도형을 그리는 것이기 때문에

하위에 아무 것도 없다.

SVG는 다양한 도형을 태그로 생성하여 표현하는

차이를 확인할 수 있다.


이상의 코드는 몇 가지 문제가 있다.

먼저, 하나의 도형을 구현하는데 너무 많은 코드가 작성되었다.

작성할 코드가 많다는 건 일하는 시간이 많다는 의미이자

문제(버그)가 생길 가능성이 높다는 의미도 된다.


두 번째로 SVG는 HTML 태그를 이용하여 구현하기 때문에

CSS를 사용할 수 있는데 이용하지 않았다.

Canvas는 JS만 이용하여 작업하기 때문에

디자이너가 수정하기 어렵다.

CSS를 이용할 수 있다는 것은

디자인 관련 작업을 웹 디자이너나 퍼블리셔의 영역으로

넘길 수 있다는 것을 의미한다.

개발도 쉽게 할 수 있다.


이 두 가지 문제를 해결하는 방법은

다음 코드와 같이 함수화 하고,

CSS로 구현하면 된다.

clock3.html

먼저, 기본적으로 하나의 도형은

생성하고

부모를 지정하고

속성을 부여하여 사용한다.

따라서

무엇을 생성 할지 (eleType),

누가 부모인지(parent) 알려 주면 된다.

사용될 속성과 개수는 도형에 따라 다르지만

속성 이름(cx, cy 등)과 값의 구조를 가지는

Json을 이용하면 쉽게 처리 할 수 있다.

즉, createElement란 함수를 생성하고

파라미터로 도형종류, 부모, 속성을 지정하면

다양한 도형을 한 줄로 생성하여 사용할 수 있다.


다음으로 사용된 속성들은 2가지로 나눌 수 있다.

fill, stroke-width, stroke과 같이 고정된 값을 가지는 속성과

cx, cy, r과 같이

상황에 따라 바뀌는 값을 가지는 속성으로 나눌 수 있다.

고정된 값을 가지는 속성은 CSS의 클래스(face)로 처리하고 [라인 2]

바뀌는 값은 setAttribute으로 지정해 준다 [라인 23].






삼각함수로 구현한 예제를 (SVG I)

앞서 정리한 코드로 (SVG II)

다시 작성하면 다음과 같다.

clock4.html

먼저 CSS 클래스를 3개 작성하였다.

시계 구조(face), 시간(number), 시계 바늘(hands)에 대한

고정된 디자인 값을 가지고 있다 [라인 2 ~ 15].


drawFace에서 두 개의 원(circle)을

createElement로 간단하게 생성하였다.

하나는 시계의 외곽선 [라인 34],

나머지는 시계의 중심을 그리는 원이다 [라인 35].

외곽선은 CSS 클래스를 이용하였고(face)

중심 원은 검은색(#000)으로 채웠다.


시간 표시는 (drawNumbers)

글자를 출력하던 것을 (fillText)

Text 태그를 생성하여 구현한다 [라인 44].

Text 태그에 글자를 넣기 위해

createTextNode를 이용하는 불편함이 있다 [라인 46].


마지막으로

시계 바늘은 선을 그리던 것을 (lineTo)

라인(line) 태그를 생성하였다 [라인 76].


정리하면

Canvas에서는 필요한 도형을 함수를 이용하여 그렸고

SVG에서는 필요한 도형을 태그로 생성했다.

그리고, CSS로 디자인 부분을 분리하였다.


이상의 내용을 타이머를 이용하여

시계처럼 실행하면 문제가 생길 수 있다.

Canvas에서는

타이머가 1초마다 drawClock 함수를 호출하여

시계 구조, 시간, 시계 바늘을

모두 다시 그리기 때문에 중복 되어 실행해도 문제가 없다.


SVG에서는 모두 태그로 생성하게 되는데

1초마다 모든 것을 새로 생성하면

무한대로 계속 생성하기 때문에 여러가지 문제가 생기게 된다.

따라서, 시계 구조, 시간, 시계 바늘을 한 번 생성한 후,

매 초마다 시계 바늘의 위치만 바꾸는 방식으로 구현해야 한다.

var clock = document.getElementById('clock');
var center = parseInt(clock.style.height) / 2;
var radius = center * 0.90;

drawFace(radius);
drawNumbers(radius);

var hourHand = drawHand(0, 0, radius*0.07);
var minuteHand = drawHand(0, 0, radius*0.07);
var secondHand = drawHand(0, 0, radius*0.02);

drawClock();

function drawClock() {
    drawTime(radius);
    setTimeout(drawClock, 1000);
}

~~생략 ~~

function drawTime(radius){
    var now = new Date();
    var hour = now.getHours();
    var minute = now.getMinutes();
    var second = now.getSeconds();
    var pos = radius*0.5;
   
    //hour
    x = pos* Math.cos(Math.PI* ((hour*30)- 90 + 30/60*minute + 30/60/60*second) / 180);
    y = pos* Math.sin(Math.PI* ((hour*30)- 90 + 30/60*minute + 30/60/60*second) / 180);
    setAttributes(hourHand, {"x1": center, "y1": center, "x2": x+center, "y2": y+center});

    //minute
    pos = radius*0.8;
    x = pos * Math.cos(Math.PI* ((minute * 6)- 90 + 6/60*second)/ 180);
    y = pos * Math.sin(Math.PI* ((minute * 6)- 90 + 6/60*second)/ 180);
    setAttributes(minuteHand, {"x1": center, "y1": center, "x2": x+center, "y2": y+center});
   
    // second
    pos = radius*0.9;
    x = pos * Math.cos(Math.PI* ((second * 6)- 90)/ 180);
    y = pos * Math.sin(Math.PI* ((second * 6)- 90)/ 180);
    setAttributes(secondHand, {"x1": center, "y1": center, "x2": x+center, "y2": y+center});
}

~~생략 ~~

function setAttributes(ele, attributes) {
    for (var item in attributes) ele.setAttribute(item, attributes[item]);
}

clock5.html

drawClock 함수에서 생성하던 것을

모두 함수의 밖(앞)으로 빼서 한번만 실행되도록 하고

타이머가 drawClock를 매초마다 실행하게 작성한다.


새로 작성한 setAttributes는

Json의 값(도형들의 속성)을 주어진 개수만큼 반복해서

도형에 속성들을 부여하는 역할을 하는 함수이다.


이 함수를 (setAttributes)

시계 바늘을 생성하던 drawHand 함수 대신 사용했다.

즉, 생성하는 것이(drawHand) 아닌

속성을 바꾸는 것으로 작성한 것(setAttributes)이다.

속성을 바꾸기 위해

시계 바늘을 시, 분, 초로 구별하여 알아야 하기 때문에

생성 후 변수(hourHand, minuteHand, secondHand)로 받아서 사용한다.


 


이번에는 D3로 구현해 본다.

D3는 데이터 시각화 프레임워크로 알려져 있는데

SVG(Scalable Vector Graphic)를 쉽게 사용하도록 해준다.

자세한 내용은 찾아보길 바라고

SVG와 D3의 사용법과 변환에 대한 설명은 여기를 참고 하면 된다.


SVG와 D3의 관계는

자바스크립트와 JQuery의 관계와 유사하다.

한마디로 쉽게 사용하게 해준다는 것이고

실제로 D3 사용법은 JQuery사용법과 비슷하다.


D3와 관련해서 여기서 알아야 할 것은

getElementById 대신에 select를 사용한다 [라인 3].

createElementNS와 appendChild로 생성하던 것을

append로 한번에 생성한다.

setAttribute함수 대신에 attr함수를 사용하고

style 속성이 아닌 style 함수를 사용한다.

attr와 style 함수는 달라진 것이 없는 것 같지만

Json을 이용하여

한번에 많은 속성이나 style을 지정할 수 있다.

(setAttributes 함수 처럼)


그리고 편리한 method chaining이 있다.

이것은 다음 코드에서 보는 것처럼

지정된 노드(태그)에 메소드(함수)를 이어서 사용하는 것이다.


이러한 기능으로 인해

중복 코드 처리를 위해 작성한

createElement, setAttributes 함수가 필요 없다.

clock6.html

먼저 D3를 사용하기 위해 d3.js파일을 포함해야 한다.

간단하게 배포하기 위해

D3 사이트에서 제공하는 파일을 연결했다 [라인 1].


d3.select로 SVG 태그를 찾아서 clock변수에 지정한다 [라인 3].

그리고, 필요한 도형들을 추가(append)로 간단하게 생성한다.

예로, 원(circle)을 추가만으로 생성하고

메소드 체인으로 속성을 지정하였다 [라인 22].

attr("cx", center).attr("cy", center).attr("r", radius).attr("class", "face")

이렇게 생성할 수도 있지만

23라인과 같이

필요한 속성을 Json으로 묶어서 한번에 처리할 수도 있다.

일부러 22와 23라인 코드를 두가지 방식으로 구현하였으니

선호하는 방식을 이용하면 된다.


D3에서는 문자열을 출력하기 위해

TextNode를 생성하지 않고

Text 함수를 호출한다 [라인 32].

Text 생성은 메소드 체인과 Json을 같이 사용하여 간단하게 생성하였다.


마지막으로 시, 분 초 시계 바늘을

매초마다 이동시키는 것도

각 변수 속성을 Json으로 한번에 지정하였다 [라인 47, 53, 59].


이 정도면 D3의 장점과 특징을 간단하게 나마

파악하고 익힐 수 있을 것이다.


지금까지 아날로그 시계를

Canvas에서

기준 축을 이동시켜서 구현하는 방법

삼각 함수로 계산해서 구현하는 방법을 정리하였다.

그리고, 삼각 함수로 구현 것을 SVG와 D3로 구현하였다.


하나의 아날로그 시계를 구현하는데,

Canvas에서는 그림처럼 그렸고

SVG에서는 태그를 생성하였다.

D3에서는 몇가지 함수를 호출 하는 것으로 SVG를 구현하였다.

이상의 코드들을 자세히 본다면

Canvas, SVG, D3처럼 개발하는 도구(프레임워크 또는 개발언어)는 바뀌어도

변하지 않는 것(알고리즘, 수식 등)이 있다는 것을 알 수 있다.

변하지 않는 것을 공부하는 것이 중요할 것이다.


이외에도 자바 스크립트로 DIV를 생성해서 구현할 수도 있다.

자바 스크립트 대신에 JQuery로 구현할 수도 있다.

여기에서는 정리하지 않지만

직접 해보면 실력 향상에 많은 도움이 될 것이다.




자바 스크립트(JavaScript)를 이용하여

간단한 TreeGrid를 제작해 본다.

TreeGrid는 다음 그림(EasyUI TreeGrid)처럼

데이터를 트리와 그리드(List)의 결합 형태로 출력하는

라이브러리를 의미한다.


여기서는 실제로 사용할 수 있는 TreeGrid를 만드는 것이 아니라

간단한 TreeGrid를 단계별로 구현해 보면서

TreeGrid원리와

자바스크립트, Json 사용법 등을 설명하려고 한다.


실제 사용은 TreeGrid jQuery, EasyTree

무료(MIT) 라이브러리 등이 많이 있으니

이것을 그대로 사용하는 것이 좋고,

여기서는 간단하게 TreeGrid를 만들어 보면서

실력을 향상 시키는 것이 목적이다.


예제도 이 라이브러리에서 사용된 일부 코드를 가져와서 구성했으며

본 내용을 다루는 기본을 이해하고

차후 이 라이브러리를 분석해 보면 실력 향상에 도움이 될 것이다.


기본적으로 자바 스크립트, JQuery, JSON의 기본적인 사용법은 알고 있어야 하고,

모르는 경우 찾아보길 바란다.


구성 단계는 다음과 같이 6단계로 세분화하여 정리하였다.

  1. 트리용 데이터(JSON)을 화면에 출력
  2. 데이터(JSON)를 트리형(계층형)으로 보여 주기 위해 들여쓰기
  3. 라이브러리화
  4. 클릭시 자식 노드 숨기기
  5. 클릭시 자식 노드 숨김 / 보이기
  6. 버그 수정

이 단계를 거쳐도 실제적인 TreeGrid를 만들지는 못한다.

TreeGrid를 제작하는 원리를 익히며

자바스크립트(JavaScript)와 JQuery, Json 사용법 등을 익히는 것이 목적이다.


트리 그리드(TreeGrid)를 만들기 위해

다음 그림과 같이 데이터를 화면에 출력하는 방법부터 시작한다.


그림과 같이 3개의 데이터를 화면에 출력하기 위해서는

다음의 html처럼

테이블(table) 태그를 사용해서 웹 페이지를 작성한다.

이렇게 웹 페이지에 테이블 태그를 작성해 두고 사용하는 것은

고정된 문서(구성)으로 사용하는 것으로,

트리 형태로

데이터를 들여쓰기 하고

하위 노드를 숨겼다 보여주는 동적인 기능을 구현하기 어렵다.

따라서, 다음 코드와 같이

자바스크립트(JavaScript, JS)를 이용하여

테이블 태그를 생성하고 조절하여

동적 웹 페이지를 제작하게 된다.
 

sample1.html


먼저, 11 라인과 16 라인에서

option과 data라는 배열 변수를 선언하고 초기값을 주었다.

option변수는 트리 그리드의 각 열(column)을 필드로 보고

각 필드의 특성을 지정하는 변수로 사용한다.

즉, 배열 크기가 3 개 이므로 3 개의 필드를 사용한다.

각 배열은 Json으로 구성되었고,

필드의 구체적인 정보를 담고 있다 (예제라 width 하나 뿐임).

필드명은 작업명(task), 시작일자(startdate), 종료일자(enddate)이고

각각의 크기는 200px, 100px, 100px로 지정하였다.

이 필드들은 테이블 태그의 열(td)로 생성되게 된다.


배열 data 변수는 그리드에 출력한 데이터를 담은 변수로

하나의 인자가 하나의 데이터, 즉 행(row, line)이 되고

예제에서 배열 크기가 3 이니 데이터 개수는 3 이 된다.

필드 구성은 앞서 구성한 option 변수에서 선언한 필드에

값을 주어 구성한다.

option 변수에서 선언하지 않은 필드를 사용해도 되며,

이 경우는 숨겨진 필드로

보이진 않지만 프로그램 처리상 필요한 역할을 할 수 있다.

option과 data 변수는

필드명(task, startdate, enddate)으로 서로 찾게 되고

data 변수의 값들을 출력하면서,

option의 속성(width)을 가져와서 화면을 구성하게 된다 (31, 32 라인).


다음으로 코드의 23~25 라인에 사용된 JQuery는

동적으로 테이블(Table) 태그를 생성하고,

id가 tree(#tree)인 div (39 라인)에 추가하는 코드이다.

이렇게 해서 지정된 위치(사용자가 원하는 위치)에 TreeGrid가 생성된다.

마지막으로 27 ~34 라인이 데이터를 화면에 출력하는 코드이다.

데이터를 화면에 출력하는 방법은

데이터의 개수만큼 반복하면서(for, each)

한 데이터가 가진 필드들(for, each)의 값을 출력하면 된다.

출력 방식은 테이블 테그를 사용하기 때문에

행은 TR,

열은 TD를 동적으로 생성하여 구현하게 된다.


즉, 데이터(data) 변수의 크기 만큼 반복하면서 $.each( data, function( index, row)

TR 태그를 생성해서 행을 출력하고 var tr = $("<tr>").appendTo(table)

필드 개수만큼 반복해서 $.each( option, function( i, fieldInfo)

TD 태그를 생성한다. var td = $("<td>").appendTo(tr)

앞서 option 과 data 변수는

필드명(task, startdate, enddate)으로 서로 찾는 다고 설명했는 데,

생성한 TD에 필드 값을 출력하기 위해

option의 필드명(field)으로

현재 data의 출력하려고 하는 행(row)에서 값을 찾아와 row[fieldInfo.field]

TD에 넣어준다(html).


참고: 두개의 each문이 사용되었다.

$.each( data, function( index, row)
$.each( option, function( i, fieldInfo)


배열 변수인 data, option의 개수 만큼 반복해서 실행하는 JQuery 문법으로

콜백 함수의 첫 번째 파라메터 index와 i는 배열의 몇 번째 데이터인지

row와 fieldInfo는 배열 중 각 인자의 값이 넘겨 진다.

data는 각각의 행(노드 값)이 넘어오고 하나의 행은 Json으로 구성되어 있다.

option는 각 필드 정보의 행이 Json으로 넘겨진다.

따라서 each문 안에서

option은 fieldInfo.field, fieldInfo.width로 사용되고

data는 row.task, row.startdate, row.enddate로 사용된다.

또, Json 데이터는 fieldInfo["field"], row["task"] 같이

배열처럼 사용할 수 있다.

31 라인에서는 이 코드를 혼용해서 사용했다. td.html( row[fieldInfo.field]);


이상의 코드를 실행하면,

앞서 HTML의 테이블 태그로만 작성된 코드가 생성된다.

즉, 두 개의 코드를 실행한 결과가 같다는 것으로

하나는 HTML만으로,

하나는 JS를 이용해서 생성한 것이다.

웹 브라우저의 DOM 탐색기(F12)를 실행해서

JS로 생성된 HTML코드를 확인해 보자



JQuery 기초 문법 몇 가지

$("#tree")처럼 문자열에 #을 사용하면

HTML 태그 ID가

지정된 문자열(tree)과 일치하는 태그를 찾아서 반환한다.

반환된 클래스는 HTML 태그의 기능 외에

JQuery에서 제공하는 부가적인 기능도 추가되어 있다.

즉, JQuery 문법으로 클래스를 사용하게 된다.


$("<table>")처럼 꺽쇠(<>)를 사용하면

해당 HTML 태그를 생성해서 JQuery 클래스가 반환된다.


여기에는 사용되지 않았지만(다음 예제들에 나옴)

$(".treegrid9-1")처럼 점(.)을 사용하면

해당 문자열을 CSS의 클래스 명(Classname)으로 사용하는

HTML 태그를 찾아서 JQuery 클래스로 반환된다.

#과 유사하게 사용되는데

#은 ID를 찾는 것이고

점(.)은 클래스 명에서 찾는 것이다.


주의: 자바 스크립트의 클래스와 CSS의 클래스를 헷갈린다면 TreeGrid 제작 관련 내용이 도움이 안 될 수도 있다.

JQuery에 대한 간단한 설명은 검색을 해보거나 여기를 참조하면 된다.


데이터(JSON)를 트리형(계층형)으로 보여 주기 위해

다음 그림과 같이 들여쓰기를 구현해 본다.

Job2는 Job1의 자식으로 처리하고

그림과 같이 부모인 Job1보다 뒤로 들여쓰기 하여

자식이라는 것을 표현하였다.

트리 구조가 표현되게 되는 것이다.

이러한 표현을 하기 위해서는

가장 먼저 데이터를 구성할 때

부모 / 자식 관계를 제대로 표현해 줘야 하는데

자식 노드(TR)에서 부모 노드(TR)를 지정하는 방식

부모 노드(TR)가 자식 노드(TR) 리스트를 보유하는 방식

2 가지 방식으로 저장 할 수 있다.


먼저, 자식 노드에서 부모 노드를 지정하는 방법을 알아본다.

개념을 정리해 보면

자식에서 부모를 지정하려면

부모의 고유 식별자(ID)가 있어야 한다.

그래야 특정 노드가 내 부모 임을 표현할 수 있기 때문이다.

따라서, 각 노드(데이터 - data)는

자신을 나타내는 고유 식별 값인 ID와

자신의 부모가 누구 인지 나타내기 위해

부모의 고유 식별 값 ID를 나태내는 값(Parent)이 있어야 한다.


이 값을 가지고

화면 출력을 위해 노드(TR)를 생성할 때

부모가 없으면 노드만 생성하면 된다.

부모가 있으면 최상위 부모(최초 조상-단군)를 찾아서

최상위 부모로부터 몇 대손(depth)인지 파악하고

그 깊이(depth) 만큼 부모로 부터 들여쓰기 해주면 된다.


sample2.html


코드 21 라인에 있는 data 변수의 값을 확인해 보면

앞서의 sample1과 다르게

id와 parent 필드가 추가된 것을 알 수 있다.

앞서 정리한 개념을 처리한 것으로

부모 / 자식 관계를 표현하기 위해 추가하였다.

id 필드의 값은 데이터 내에서 고유한 값이어야 하고,

parent 필드의 값은 나의 부모의 id 값이 저장 된다.


다음은 이렇게 저장한 데이터를 적절하게 화면에 출력하는 것이다.

먼저, 데이터 간의 부모 / 자식 관계를

화면에 표현해야 하는데

데이터 하나가 테이블(TABLE) 태그의 TR로 표현되기 때문에

TR 태그 간의 관계로 구성하게 된다.

다음 단계에서 구현할 접는 기능을 위해서도 필요하다.

즉, TR 태그에 부모 / 자식 관계가 표현되어야 하는데

CSS 클래스명(class속성)으로 처리한다.

HTML 태그의 ID 속성으로 처리하기도 하는데

ID 속성은 이런 저런 데이터를 같이 저장하는 것이 어려워서

대부분 ID 속성 보다는 CSS 클래스명을 이용한다.

TR 태그에도 자기 ID(treegrid9-)와

부모 ID(treegrid9-parent-)가 같이 저장되어야 하기 때문이다.

(이해가 안될 경우 이 글 마지막 그림 참조)


참고: 데이터 값을 식별하는 ID와 HTML 태그의 ID 속성을 혼동할 수 있는데

다른 것이니 잘 구별해서 사용해야 한다.

데이터 값을 식별하는 ID는 고유 값을 가진다는 걸

표현하기 위해 자주 사용하는 변수 이름으로 다른 명칭을 사용해되 된다.


34 라인에 JQuery로 TR을 생성하면서 $("<tr>")

데이터의 id(row.id)와 문자열("treegrid9-")을 합쳐서

클래스를 추가(addClass)하여 TR마다 고유 이름(id)를 부여한다.

데이터의 ID만 사용해도 되지만,

개발한 TreeGrid가 라이브러리 형태로 제공되면

다양한 다른 페이지들과 같이 사용되고

그럴 때 다른 사람이 작성한 클래스명과 같은 경우가 생겨,

문제가 발생할 수 있기 때문에

식별 문자열(treegrid9-)을 추가해서 작성하였다.


참고: data변수에 사용된 id와 parent는 option 변수에 없다.

즉, id와 parent는 화면에 출력되지 않고 숨겨진 필드처럼 사용 된다.


36 라인에서 부모가 있으면(row.parent)

부모의 다음 행에 추가하고(tr.insertAfter)

부모가 없으면 테이블에 추가한다 tr.appendTo(table).

부모를 찾을 때 클래스 명으로 찾는데

JQuery를 이용해서 찾는다.

클래스 명은 점(.)을 이용한다. $(".treegrid9-" + row.parent)


부모 다음에 출력하고 나면

클래스 명에 자신의 부모가 누구라고 표시해야 한다. tr.addClass("treegrid9-parent-" + row.parent);

다만 부모 ID(treegrid9-?)를 그대로 사용하면

명명 규칙이 같아서

어느 것이 자신의 ID이고

어느 것이 부모를 의미하는지 구분할 수 없어

식별 문자열을

자신의 ID는 treegrid9- 으로

부모는 treegrid9-parent- 으로 사용한다.


39 라인의 getDepth 함수에 대한 구체적인 설명은 생략하니 분석해 보길 바란다.

getDepth 함수는

현재 자신이 root로부터 몇 번째 자식인지 계산하는 함수이다 (depth).

원리는 현재 자신(TR)의 클래스 명에 부모가 있는지 확인해 보고

있으면 부모의 ID만 추출하고(parentPattern)

추출된 부모의 ID를 식별문자열(treegrid9-)과 결합해 부모를 찾는다.

다시 그 부모의 클래스 명에서

부모가 있는지 확인해 보고

있으면 부모의 ID만 추출하여 부모를 찾아가면서

부모가 없을 때까지 반복해서 while (parent)

개수를 확인하여 반환한다.


부모의 ID만 추출할때 정규식을 사용하였다.

해당 정규식은 treegrid9-parent-([A-Za-z0-9_-]+) 으로

부모를 나타내기 위해 사용한 식별문자열을 의미한다.


깊이(depth)를 알아야

필드 값을 출력할 때 들여쓰기를 할 수 있다.

깊이가 1 이면(사람으로 치면 단군 왕검의 1 대손이라는 의미)

부모보다 뒤로 몇 글자 들여쓰기 해주면 된다.

공백(&nbsp;)를 사용해도 되지만

여기서는 너비가 16px인 SPAN 태그를 이용해서 구현했다.

깊이가 1 이면 SPAN 태그 하나,

깊이가 2 이면 SPAN 태그 둘 ….

을 생성해서 들여쓰기를 구현하게 된다.


45 라인의 IF문은 if (i===0 && depth>0)

첫 번째 필드(task)만 들여쓰기 하기 위한 것이다.

앞서 43라 인에서 필드의 개수만큼 반복하도록(each) 작성했기 때문에

모든 필드에 대해서 해당 코드가 실행된다.


실행 결과를 DOM 탐색기로 확인하면

다음과 같이 클래스명(class)에

각 행(TR)의 ID와 부모 ID가 표시 되어 있는 것을 볼 수 있다.


정리하면

트리 구조로 보여 주기 위해서는

부모 / 자식 관계 설정을 해야 하고

관계 설정을 위해서는

자신을 식별하는 ID와

부모를 표시하는 Parent가 있어야 한다.

이 두 가지를 데이터에 표현하고

화면에 테이블로 구현해주면 되는 것이다.





+ Recent posts