2013년 7월 11일 목요일

display:none인 객체는 스크롤이 되지 않는다

오늘 버그를 수정하다 알게 된 새로운 사실. display:none인 객체는 스크롤 되지 않는다.

구현하고자 한 기능은 간단했다. 사용자가 버튼을 클릭하면 목록을 보여주어야 한다.
목록이 길어 스크롤이 생길 수도 있는데, 이때는 사용자가 선택한 아이템이 보이도록 스크롤 된 상태로 목록을 보여주어야 한다. 

목록은 처음에는 display:none 상태로 화면에서 보이지 않는다. 사용자가 클릭하면 display:block으로 변경하여 목록을 화면에 보여준다.

기존 로직은 다음과 같았다.

  1. 사용자가 선택했던 아이템이 목록에서 몇 번째인지 조회한다.
  2. 목록당 높이를 구해 얼마나 스크롤 해야 목록의 중간에 아이템이 보일지 픽셀을 계산한다.
  3. jQuery의 scrollTop을 이용해 스크롤을 이동한다.
  4. jQuery의 show를 이용해 css 속성을 display:block으로 변경한다.
간단하다. 하지만 스크롤은 되지 않는다. 무엇이 잘못된 것일까?
결론부터 말하면 display:none이 원인이다. "CSS Display and Visibility"에 display:none에 대한 설명이 다음과 같이 나와 있다.
display:none hides an element, and it will not take up any space. The element will be hidden, and the page will be displayed as if the element is not there:
display:none은 해당 요소가 공간을 차지하지도 않고 페이지를 렌더링할 때 마치 해당 요소가 거기에 없는 것 마냥 취급한다고 한다.

반면 같은 문서에 visibility:hidden에 대해서는 다음과 같이 설명하고 있다.
visibility:hidden hides an element, but it will still take up the same space as before. The element will be hidden, but still affect the layout.
display:none과는 해당 요소를 감추는 것까지는 같지만 여전히 공간을 차지하며 레이아웃에 영향을 끼친다고 되어있다.

그렇다면 브라우저는 display:none인 요소를 어떻게 처리할까? 이와 관련된 내용은 "Rendering: repaint, reflow/relayout, restyle"에 나와 있다. 
if you're hiding a div with display: none, it won't be represented in the render tree.
요소를 display:none으로 처리하면 해당 요소는 렌더링 트리에서 제거된다.

스크롤 이동은 css 속성으로 지정하는 것이 아니다. 그냥 렌더링이다. 이처럼 스크롤하려는 요소가 렌더링 트리에서 빠져있으면 scrollTop을 호출해도 아무런 일도 벌어지지 않는다. 반면 visibility:hidden으로 처리된 요소는 렌더링 트리에 존재하므로 스크롤이 되는 것이다.

이와 비슷한 이유로 jQuery의 offset 함수는 display:none인 요소에 대해서는 동작하지 않는다. ".offset API"를 보면 다음과 같이 나와 있다.

Note: jQuery does not support getting the offset coordinates of hidden elements or accounting for borders, margins, or padding set on the body element.
While it is possible to get the coordinates of elements with visibility:hidden set, display:none is excluded from the rendering tree and thus has a position that is undefined.

렌더링 트리에서 제외되어 있기 때문에 요소가 실제 어느 위치에 있는지 알 수가 없는 것이 이유인 듯하다.

아무튼, 결론은 display:none인 요소는 렌더링 트리에 존재하지 않아 스크롤 되지 않는다.




[HTML5 Game Development] Canvas

게임을 만들려면 2D 렌더링 엔진이 필요하다. HTML5에서는 HTML 엘리먼트 Canvas를 이용해 렌더링을 처리 할 수 있다.



그렇다면 Canvas란 무엇일까?

Canvas는 HTML5에 추가된 엘리먼트로 API를 통해 그래프, 이미지, 텍스트 등을 캔버스에 그릴 수 있다. Canvas는 HTML 엘리먼트로 두 개의 속성을 갖는다. width, height.
width, height 속성을 이용해 캔버스의 크기를 지정할 수 있다.

캔버스 생성하기
다음처럼 코드를 작성하고 브라우저에서 열면 빈 화면만 나오겠지만 캔버스가 생성된다. 여기서 살펴볼 점은 canvas에서 getContext 메서드를 호출하는 부분이다. 이름을 보니 Context를 반환하는 메서드인듯하다.

<html>
<body>
<canvas id="my_canvas"></canvas>
</body>
<script>
var canvas  = null,
    context = null;

function setup() {
    canvas = document.getElementById("my_canvas");
    context = canvas.getContext("2d");
    canvas.width = 300;
    canvas.height = 500;
}

setup();
</script>
</html>

Context란 무엇일까?
getContext를 호출하면 drawing context가 반환되는데 우리는 drawing context의 API를 호출해 Canvas에 우리가 원하는 그림을 그릴 수 있다.
getContext를 호출할 때 "2d"를 인자로 넣어 CanvasRenderingContext2D를 얻어온다. 그리고 CanvasRenderingContext2D의 메서드를 호출하면 된다.
Name & ArgumentsReturnDescription
getContext(in DOMStringcontextId)RenderingContextReturns a drawing context on the canvas, or null if the context ID is not supported. A drawing context lets you draw on the canvas. Calling getContext with "2d" returns aCanvasRenderingContext2D object, whereas calling it with "experimental-webgl" (or "webgl") returns a WebGLRenderingContext object. Thicontext is only available on browsers that implement WebGL.
출처:https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement

getContext를 호출할 때 "2d"를 인자로 넣으면 CanvasRenderingContext2D가 반환된다. 그리고 필요한 CanvasRenderingContext2D API를 호출한다.

Canvas에 이미지를 올려보자

이미지를 canvas에 올리려면 3가지가 필요하다

  • Image() 객체를 생성한다.
  • onload 함수를 구현한다.
  • Image.src에 이미지 URL을 세팅한다.

<html>
<body>
<canvas id="my_canvas"></canvas>
</body>
<script>
var canvas  = null,
    context = null,
    img  = null;

function setup() {
    canvas = document.getElementById("my_canvas");
    context = canvas.getContext("2d");
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    
    img = new Image();
    img.onload = onImageLoad;
    img.src = "./1373428533_Armed_robot.png";
}

function onImageLoad() {
    console.log("load");
}
setup();
</script>
</html>


이 소스를 실행하면 콘솔창에 "load"라고 뜰 뿐 이미지가 그려지진 않는다. 일단 이전 소스와 바뀐 점은 canvas의 width와 height를 고정값이 아닌 브라우저 사이즈로 변경했다. 그리고 Image객체의 인스턴스를 생성했으며 img의 onload 함수를 구현했고 src에 이미지 경로를 세팅했다. 이미지를 canvas에 올리는 데 필요한 3가지를 다 했지만 Canvas에 그림은 여전히 그려지지 않는다.
조금 전 Context를 설명하면서 Canvas에 그림을 그리려면 Context의 메서드를 이용한다고 했었다. 이 소스에서 빠진 부분이 바로 그 부분이다.
이제 onImageLoad 메서드를 다음처럼 수정해보자.

function onImageLoad() {
    context.drawImage(img, 00);
}

수정하면 다음 캡쳐화면처럼 이미지가 정상적으로 그려진다.


drawImage 메서드를 호출할 때 첫 번째 인자를 img 객체이고 두번째, 세번째는 x, y좌표이다. 즉 캔버스 어느 위치에 이미지를 그릴 것인지 지정하는 것이다.
drawImage에 대한 상세한 설명은 API를 참고하도록 한다.

캐릭터를 움직이게 만들어보자

캐릭터를 움직이는 것처럼 만드는 방법은 의외로 간단하다. 일정 시간 간격으로 캔버스 위에 이미지를 그리면 된다. 이는 setInterval 함수를 이용하면 가능하다. setInterval 함수는 우리가 지정한 시간마다 우리가 지정한 함수를 호출해한다.
제일 먼저 이미지 경로를 선언해놓는다.

var assets = ["img/robowalk00.png",
              "img/robowalk01.png",
              "img/robowalk02.png",
              "img/robowalk03.png",
              "img/robowalk04.png",
              "img/robowalk05.png",
              "img/robowalk06.png",
              "img/robowalk07.png",
              "img/robowalk08.png",
              "img/robowalk09.png",
              "img/robowalk10.png",
              "img/robowalk11.png",
              "img/robowalk12.png",
              "img/robowalk13.png",
              "img/robowalk14.png",
              "img/robowalk15.png",
              "img/robowalk16.png",
              "img/robowalk17.png",
              "img/robowalk18.png"];

이제 이미지 경로를 이미지 객체로 만든다.

var frames = [];

for (var i = 0; i < assets.length; i++) {
    var img = new Image();
    img.src = assets[i];
        
    frames.push(img);
}

캔버스에 이미지를 그리는 함수를 정의한다.

var idx = 0;

function animate () {
    context.clearRect(00, canvas.width, canvas.height);
    
    context.drawImage(frames[idx++]00);
    idx %= frames.length;
}

변수 idx에는 현재 캔버스에 그린 이미지 번호가 저장되며 배열의 끝까지 도달하면 변수의 값은 다시 0으로 돌아간다.
animate함수 맨 첫 줄에 clearRect를 호출하는 이유는 이전에 그려진 이미지를 지워야하기 때문이다. 지우지 않으면 이미지 위에 이미지가 또 그려져 움직이는 효과를 내지 못한다.
이제 animate 함수를 setInterval로 호출하는 일만 남았다.

전체 소스는 다음과 같다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
    <canvas id="my_canvas"></canvas>
</body>
<script>
var assets = ["img/robowalk00.png",
              "img/robowalk01.png",
              "img/robowalk02.png",
              "img/robowalk03.png",
              "img/robowalk04.png",
              "img/robowalk05.png",
              "img/robowalk06.png",
              "img/robowalk07.png",
              "img/robowalk08.png",
              "img/robowalk09.png",
              "img/robowalk10.png",
              "img/robowalk11.png",
              "img/robowalk12.png",
              "img/robowalk13.png",
              "img/robowalk14.png",
              "img/robowalk15.png",
              "img/robowalk16.png",
              "img/robowalk17.png",
              "img/robowalk18.png"];
             
var frames = [],
    idx = 0,
    canvas;

function setup() {
    canvas = document.getElementById("my_canvas");
    context = canvas.getContext("2d");
    
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    
    for (var i = 0; i < assets.length; i++) {
        var img = new Image();
        img.src = assets[i];
        
        frames.push(img);
    }
    
    setInterval(animate, 1000/30);
}

function animate () {
    context.clearRect(00, canvas.width, canvas.height);
    
    context.drawImage(frames[idx++]00);
    idx %= frames.length;
}

setup();
</script>
</html>

* 이 자료는 udacity의 HTML5 Game Development 강의를 듣고 공부한 내용을 정리한 것이다.

2013년 7월 7일 일요일

AngularJS: An Overview (번역)

AngularJS는 복잡한 클라이언트 사이드 애플리케이션 개발을 위해 구글에서 만든 자바스크립트 프레임워크입니다. Angular의 죽여주는 기능은 지시자(directives)로, 자신만의 태그와 속성을 만들어 HTML을 확장할 수 있게 해줍니다. Angular 프로젝트는 여타 다른 자바스크립트 MVC 프로젝트와는 다소 차이가 있지만, 일단 Angular의 구조를 이해하기만 한다면 굉장히 모듈화하기도 쉽고 유지보수하기도 좋습니다. 이제 AngularJS의 주요 컴포넌트와 이들이 어떻게 동작하고 왜 여러분이 Angular를 다음 프로젝트에서 진지하게 고려해봐야 하는지 알아보겠습니다.


AngularJS의 철학


데이터 중심
jQuery 같은 자바스크립트 라이브러리에 익숙한 사람이 AngularJS를 사용하려면 약간의 패러다임의 변화가 필요합니다. AngularJS는 jQuery와 유사해 보이지만 그 반대입니다. 다시 말하면 jQuery는 DOM 조작에 초점이 맞춰져 있어 우리는 DOM을 보고 데이터를 갱신합니다. 반면 Angular에서는 우리가 데이터를 갱신하면 이에 맞춰 DOM을 자동으로 갱신시켜 주는데 이러한 특징 덕분에 결과적으로 개발자의 일이 줄어듭니다.

테스트 용이
AngularJS는 테스트하기 좋게 설계됐습니다. 의존성 주입(Dependency Injection)을 사용하여 많은 객체 중 어느 부분이든 Mock을 생성하기가 편리합니다. 또한 애플리케이션을 개발할 때 기능을 여러 개의 작은 부분으로 나눠서 개발하기 편리하도록 설계되어 테스트가 용이해집니다.

HTML 선언
Angular는 복잡한 웹 애플리케이션을 개발 할 때 HTML을 확장하여 개발에 필요한 언어가 될 수 있도록 설계되었습니다. 또한 자신만의 태그와 속성을 추가할 수 있어 복잡한 작업을 간단한 HTML 태그로 처리할 수 있습니다.

데이터 바인딩

간단한 AngularJS 앱을 살펴보겠습니다. 이  예에서는 두 가지 데이터 바인딩을 사용합니다. 

index.html

1
2
3
4
5
6
<body ng-app>
  <div>
    <input type='text' ng-model='name' />
    <h2>{{name}}</h2>
  </div>
</body>

이 예제에서 어떤 일이 일어나는지 한 줄 한 줄 살펴보겠습니다.
  • <body ng-app>: 모든 Angular 코드는 ng-app 지시자로 감싸야합니다. 이렇게 감싸는 것의 의미는 이 태그 안의 모든 것이 Angular 애플리케이션으로 처리한다는 것을 뜻합니다.
  • <input type="text" ng-model="name"/>: 이 부분은 데이터 바인딩 방법 중 하나입니다. 여기서 사용하는 ng-model 지시자는 input 요소와 바인딩됩니다. (인자(argument)를 갖는 속성 지시자에 대해서도 다룰 겁니다.)
  • <h2>{{name}}</h2>: 우리가 input box에 입력하면 h2 태그는 자동으로 업데이트 됩니다.  바로 이 부분이 제가 전에 언급했던 DOM 자동 변경입니다. 자바스크립트 코드 한 줄 없이도 가능합니다.
모듈
모듈은 AngularJS 애플리케이션의 객체를 구성할 때 사용합니다. 모듈을 애플리케이션의 "코어"로 사용할 수도 있고, 사용되는 모든 클래스를 포함하게 할 수도 있으며, 유사한 기능을 가진 여러 객체를 묶을 때 사용할 수도 있습니다. 이제 예제에서 사용할 모듈을 만들어 보겠습니다.


app.js

1
app = angular.module('myApp', []);


index.html

1
<body ng-app='myApp'>

angular.module의 첫번째 인자로 모듈명을 넣습니다. ng-app에도 같은 이름을 지정하여 모듈에 바인딩되도록 합니다. 두번째 인자는 의존하는 다른 모듈이 있으면 그 모듈의 이름을 배열에 넣습니다. 여기서는 의존하는 모듈이 없으므로 비워둡니다. 지시자와 마찬가지로 AngularJS에서 기본적으로 만들어 둔 모듈을 사용할 수도 있습니다.

이제 모듈을 만들었으니 컨트롤러라 할 수 있는 AngularJS 객체를 처음으로 만들어 보겠습니다. 

컨트롤러

컨트롤러는 특정 HTML 요소와 연결되어 있습니다. 컨트롤러에는 데이터와 함수가 있는데 함수를 통해 컨트롤러는 HTML과 상호작용할 수도 있으며 다른 서비스 객체와 상호작용할 수도 있으며 서버와의 통신을 할 수도 있습니다. 이제 컨트롤러를 만들어 div 와 바인드해보겠습니다.

app.js

1
2
3
app.controller('mainCtrl', function($scope){
  $scope.name = 'Default Name';
});


index.html

1
2
3
4
5
6
<body ng-app='myApp'>
  <div ng-controller='mainCtrl'>
    <input type='text' ng-model='name' />
    <h2>{{name}}</h2>
  </div>
</body>

여기서 우리는 ng-controller 지시자를 사용해 div와 컨트롤러 함수를 연결시켰습니다. 컨트롤러는 기본적으로 $scope라는 인자 한 개를 받습니다. $scope는 html과 상호작용하는 데 필요한 모든 데이터를 갖고 있습니다.  이제 페이지를 새로고침하면 input이 "Default Name"으로 변경된 것을 볼 수 있을 겁니다. 그 이유는 우리가 컨트롤러에 변수를 선언했기 때문입니다.

$scope에 함수를 넣을 수도 있고 HTML에서 함수를 호출하게 할 수도 있습니다. Angular는 다양한 이벤트처리를 위해 여러가지 지시자를 제공합니다. 예제를 수정해 $scope에 함수를 추가하고 ng-click 지시자로 호출하도록 변경해보겠습니다. add 버튼을 추가해 이름을 저장한 뒤 리스트에 보여주도록 하겠습니다.


app.js

1
2
3
4
5
6
7
8
app.controller('mainCtrl', function($scope){
  $scope.name = 'Default Name';
  $scope.people = [];
  $scope.savePerson = function() {
    $scope.people.push[$scope.name];
    $scope.name = '';
  };
});


index.html

1
2
3
4
5
6
7
<body ng-app='myApp'>
  <div ng-controller='mainCtrl'>
    <input type='text' ng-model='name' />
    <button ng-click='savePerson()'>Save Person</button>
    <h2>{{name}}</h2>
  </div>
</body>

이제 사람 목록을 저장할 수 있습니다. 이제 우리가 저장한 사람의 목록을 출력하기 위해 또 다른 지시자인 ng-repeat을 사용해보도록 하겠습니다.


ndex.html

1
2
3
<ul>
  <li ng-repeat='person in people'>{{person}}</li>
</ul>

지금까지 자바스크립트로 데이터를 어떻게 변경하는지를 살펴봤으니 이제는 우리만의 지시자를 생성해서 어떻게 DOM을 변경할 수 있는지를 알아보겠습니다.

지시자(Directives)

이미 예제 애플리케이션에서 여러 개의 지시자를 사용했습니다. 지시자는 우리가 직접 정의할 수도 있는데, 이제 "alertable"이란 지시자를 한 번 만들어보겠습니다. 이 지시자는 HTML 요소를 클릭할 때마다 얼럿 메시지를 보여주는 기능을 합니다.

app.js
1
2
3
4
5
6
7
8
9
10
app.directive('alertable', function(){
  return {
    restrict : 'A',
    link: function(scope, element, attars) {
      element.bind('click', function() {
        alert(attrs.message);
      });
     }
  };
});
그리고 인원 목록에 추가합니다.

index.html

1
2
3
<li ng-repeat='person in people'>
  <span alertable='{{person}}'>{{person}}</span>
</li>
보는 것처럼 예제에서는 지시자를 정의한 객체를 반환합니다. 전달할 수 있는 인자는 여러 개가 있지만 여기서는 예제에서 사용한 두 개의 인자만 살펴보도록 하겠습니다.
  • restrict: 어떤 종류의 지시자인지 명시합니다. 필수로 입력해야하는 값이며 4가지 값이 있습니다.
    • E: 요소. 사용 예: <my-directive></my-directive>
    • A: 속성. 사용 예: <div my-directive></div>
    • C: 클래스. 사용 예: <div class="my-directive"></div>
    • M: 주석. 사용 예: <!-- directive:my-directive -->
  • link: link 함수는 이벤트 리스너를 추가하고 dom을 갱신하는 역할을 맡습니다.
그 외 다른 옵션은 Angular directive documentation에서 확인 할 수 있습니다.

서비스

서비스는 비지니스 로직이 있거나 데이터를 처리하는 클래스입니다. 이제 이전 예제를 리팩토링해서 사람 목록 데이터를 처리하는 서비스를 만들어 보겠습니다.


app.js

1
2
3
4
5
6
7
8
app.factory('PersonService', function() {
  var PersonService = {};
  PersonService.people = [];
  PersonService.addPerson = function(person) {
    PersonService.people.push(person);
  };
  return PersonService;
});
보는 것처럼 서비스는 컨트롤러와는 만드는 방법이 상당히 다릅니다. 컨트롤러는 단순히 함수였다면 서비스는 객체를 반환하는 함수입니다. 이 말은 원한다면 서비스에 private 메서드를 추가할 수도 있다는 의미입니다.

이제 이렇게 만든 서비스에 컨트롤러가 접근하도록 해야합니다. 이 둘은 같은 모듈에 있어 설정하기 쉬운데 이는 Angular의 마법과 같은 특징 중 하나입니다. 컨트롤러가 서비스에 접근하려면 다음처럼 서비스로 넘기면 됩니다.
1
app.controller('mainCtrl', function($scope, PersonService){});
정말로 놀라운 점은 컨트롤러 함수에서 인자의 순서를 변경해도 함수 동작에 변함이 없습니다. 즉 다음처럼 입력한다는 것입니다.
1
app.controller('mainCtrl', function(PersonService, $scope){});
이렇게 해도 함수는 완벽하게 똑같이 동작하는데 그 이유는 AngularJS는 인자의 이름을 보고 무엇을 넘길지 결정하기 때문입니다. 그렇다면 자바스크립트 코드를 minify하면 문제가 되지 않을까 걱정하실텐데요. 정확한 지적입니다. 하지만 다행히도 Angular는 코드를 minify한 후에도 문제가 발생하지 않도록 또 다른 함수 선언 방법을 제공합니다. 문자열 배열을 인자로 넘겨 인자가 무엇인지 angular에게 알려주게 하는 방법입니다. 실제 함수는 배열의 마지막에 위치시킵니다. 그러면 컨트롤러는 다음과 같은 모습일겁니다.
1
app.controller('mainCtrl', ['$scope', 'PersonService', function($scope, PersonService){}]);
Angular는 서비스를 개발할 때 서버와 통신하는 데 필요한 유용한 모듈을 제공합니다. 가장 유용한 모듈은 ngHttp와 ngResource입니다. ngHttp는 HTTP request를 생성할 때 사용하는 모듈이고 ngResource는 ngHttp를 확장시켜 REST API를 지원하게 만들어주는 모듈입니다. 이 두 모듈을 보면 왜, 언제 서비스를 선언해야 하는가를 알 수 있는데 객체를 반환하는 함수를 만들어야 한다면 ngResource처럼 객체를 확장시켜 반환하는 함수를 사용합니다.

Routing
Angular는 URL 라우팅을 지원합니다. 모둘의 config 함수를 사용해 라우팅을 설정할 수 있습니다. 예제 페인 페이지의 구조를 분리시켜 인원 목록의 사람마다 프로필 페이지를 만들 수 있습니다. 다음 예제처럼 설정하면 됩니다.

app.js

1
2
3
4
5
6
7
8
9
10
app.config(function($routeProvider){
  $routeProvider.when('/', {
    templateUrl: 'templates/home.html',
    controller: 'homePageCtrl'
  });
  $routeProvider.when('/person/:id', {
    templateUrl: 'templates/profile.html',
    controller: 'profileCtrl'
  });
});

필터
필터는 작지만 AngularJS에서 종종 유용하게 사용됩니다. 필터는 데이터는 변환시켜 사용자에게 보여줄 때 사용합니다. 지시자처럼 직접 만들 수도 있고 Angular에서 제공하는 필터도 있습니다. 다음 HTML을 살펴보도록 하겠습니다.


1
<span>Price:</span> 300

브라우저는 다음처럼 출력합니다.


1
Price: 300

여기에 필터 두 개를 "|" 연산자를 이용해서 출력을 다르게 바꿔보겠습니다.


1
<span>{{ "Price:" | uppercase }}</span> {{ "300" | currency }}

그러면 출력은 다음처럼 됩니다.


1
PRICE: $300.00


앞으로 더 읽으면 좋은 것들

이 글이 여러분들에게 도움되었길 바라지만 지금까지 다룬 내용은 Angular.js로 무엇을 할 수 있는지 겉핡기 정도로만 다뤘습니다. Angular에 대해 조금 더 알고 싶다면 다음의 자료가 도움이 되리라 생각합니다.




원본 링크: http://glennstovall.com/blog/2013/06/27/angularjs-an-overview/?utm_source=javascriptweekly&utm_medium=email