TypeScript를 이용해 canvas에 원을 움직이는 예제를 만들어 보겠습니다.
본격적으로 개발하기 위해 Visual Studio Code에 TypeScript 개발 환경을 만들 것입니다.
개발 환경 구성
TypeScript 컴파일러(tsc) 설치
tsc를 가장 쉽게 설치하는 방법은 npm을 이용하는 것입니다.
$ npm install -g typescript
정상적으로 설치되었는지 버젼을 통해 확인 합니다.
$ tsc --version
Hello World
예제 부터 시작해 봅시다. HelloWorld
디렉토리를 만든 후 VS Code를 실행합니다.
$ mkdir HelloWorld
$ cd HelloWorld
$ code .
helloworld.ts
를 만든 후 아래의 코드를 추가합니다.
let message: string = "Hello World";
console.log(message);
통합 터미널 (Ctrl + `)을 열고 tsc helloworld.ts
로 컴파일을 하면 helloworld.js
를 얻을 수 있습니다. Node.js
를 통해 실행해 볼 수 있습니다.
$ node .\helloworld.js
Hello World
파일 구성
TypeScript를 이용해 canvas
에 원을 움직이는 코드를 작성하기 위해서는 다음의 세 개의 파일이 필요합니다.
tsconfig.json
: TypeScript 컴파일러를 구성index.html
:canvas
와 원을 추가하는button
이 있는 단순한 HTML 페이지main.ts
: 우리가 작성하고 컴파일 할 TypeScript 파일
tsconfig.json
파일은 다음과 같이 구성합니다.
{
"compilerOptions": {
"target": "es5",
"sourceMap": true,
"lib": [
"es5",
"dom"
],
"noUnusedLocals": true,
"module": "commonjs"
}
}
위 설정에 대한 자세한 사항은 TypeScript 문서의 What is a tsconfig.json를 통해 확인할 수 있습니다.
index.html
파일은 다음과 같이 구성합니다. 우리가 TypeScript로 canvas
에 원을 움직이게 하기 위한 간단한 HTML 구성을 할 것입니다.
<html>
<head>
<title>Canvas Sample</title>
</head>
<body>
<canvas id="canvas" width="800" height="800"
style="border: 1px solid black;">
</canvas>
<p></p>
<button>add 10 circles</button>
<script src="main.js"></script>
</body>
</html>
main.ts
마지막으로 구현해야 할 TypeScript 파일입니다. 우리는 클래스를 이용할 것이므로 다음의 간단한 구성으로 시작합니다.
class DrawingApp {
}
new DrawingApp();
디버깅 환경 구성
웹페이지로 띄울 것이기 때문에 Live Server
를 이용해서 이를 설정합니다.
Live Server
를 VS Code 확장에서 설치 합니다.
설치되면 오른쪽 하단에 Go Live
버튼이 생성됩니다. 이 버튼을 누르면 현 디렉토리로 웹 서버를 띄우게 됩니다.
TypeScript 빌드
터미널에서 tsc
를 실행합니다.
$ tsc
tsc
를 실행하면 tsconfig.json
구성에 의해 main.js
및 main.js.map
파일이 생성됩니다.
만약 TypeScript 소스코드를 변경할 때마다 자동으로 빌드되게 하려면 tsconfig.json
의 compilerOptions
에 "watch": true
를 추가합니다.
크롬에서 디버깅
크롬 브라우저에서 디버깅을 하려면 다음의 launch.json
을 .vscode
디렉토리에 추가 합니다.
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:5500",
"webRoot": "${workspaceFolder}"
}
]
}
이때 url은 Live Server
의 실행 포트를 입력하도록 합니다.
이제 적절한 위치에 중단점 (F9)을 지정하고 디버깅 시작 (F5)
를 통해 디버깅을 시작할 수 있습니다.
샘플 코딩
디버깅 환경이 구성되었으므로 코드를 조금 바꿔 봅시다.
| main.ts
class DrawingApp {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.context = canvas.getContext("2d");
this.context.fillStyle = "blue";
this.context.beginPath();
this.context.arc(100, 100, 16, 0, 2 * Math.PI);
this.context.fill();
}
}
canvas
의 context
를 획득해서 청색 원을 canvas
에 그려보았습니다. constructor
에 canvas
객체를 넘겨줘야 하므로 index.html
도 다음처럼 변경 합니다.
| index.html
<html>
<head>
<title>Canvas Sample</title>
</head>
<body>
<canvas id="canvas" width="800" height="800"
style="border: 1px solid black;">
</canvas>
<p></p>
<button>add 10 circles</button>
<script src="main.js"></script>
<script type="text/javascript">
const app = new DrawingApp(document.getElementById('canvas'));
</script>
</body>
</html>
실행하면 다음과 같이 canvas
에 청색 원이 그려지는 것을 확인할 수 있습니다.
Live Server
는 파일을 감시해서 변경되면 웹페이지를 갱신하는 기능이 있어서 index.html
을 변경하면 웹페이지에 바로 반영을 해줍니다.
또한 tsconfig.json
의 compilerOptions
의 "watch": true
설정에 의해 main.ts
가 변경될 때 tsc
가 main.js
로 컴파일 해주고 Live Server
가 이를 감지해 웹페이지에 반영해 줍니다.
| main.js
의 this.context.fillStyle = "blue";
를 this.context.fillStyle = "red";
로 변경한 후 파일 저장시
canvas
다시 그리기
원을 이동하게 하려면 주기적으로 canvas
를 다시 그려야 하는데 웹브라우저에서 다시 그릴 수 있는 시점을 알아야 합니다. 이 때 requestAnimationFrame()
를 이용할 수 있습니다.
class DrawingApp {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
// ...
requestAnimationFrame(this.redraw);
}
private redraw() {
console.log("!!!");
requestAnimationFrame(this.redraw);
}
}
그런데 이 코드는 한번 redraw()
가 호출된 후 다시 requestAnimationFrame(this.redraw);
가 실행될 때 정상 동작하지 않습니다. 왜냐하면 콜백으로 호출된 메서드에서 this
는 DrawingApp
객체가 아닌 requestAnimationFrame()
메서드가 있는 Window
객체가 되기 때문입니다.
이를 개선하기 위해 redraw()
를 다음처럼 변경해봅시다.
private redraw = () => {
console.log("!!!");
requestAnimationFrame(this.redraw);
}
이제 잘 redraw()
가 계속해서 호출이 됩니다. this
의 작동 방식 및 특징 및 수정 방법은 this
in TypeScript를 참고하세요.
원 구조
원을 표시하려면 원의 좌표 정보 및 반지름, 색 등의 정보를 따로 정의하는 것이 좋습니다.
type Circle = {
x: number,
y: number,
readonly radius: number,
readonly color: string,
xInc: number,
yInc: number,
};
이제 원 목록을 담는 클래스 필드 및 색 관련 필드를 추가합니다.
private readonly circles: Circle[] = [];
private readonly colors: readonly string[] = ["red", "green", "blue"];
private colorsCount: number = 0;
원 관련 기능
원을 추가할 때마다 다른 색을 적용하기 위해 getColor()
메서드를 만들고
private getColor(): string {
let result = this.colors[this.colorsCount];
this.colorsCount = (this.colorsCount + 1) % this.colors.length;
return result;
}
원을 추가하는 메서드를 만들고,
public addCircles = (numbers: number) => {
var color = this.getColor();
for (var i = 0; i < numbers; i++) {
this.circles.push({
x: this.canvas.width * Math.random(),
y: this.canvas.height * Math.random(),
radius: 32 * Math.random() + 8,
xInc: 5 * Math.random() + 1,
yInc: 5 * Math.random() + 1,
color: color
});
}
}
다음은 원을 그리고 이동하는 메서드를 만듭니다.
private drawCircle(circle: Circle) {
this.context.fillStyle = circle.color;
this.context.beginPath();
this.context.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI);
this.context.fill();
}
private moveCircle(circle: Circle) {
circle.x += circle.xInc;
circle.y += circle.yInc;
if (circle.x < 0 || circle.x > this.canvas.width) {
circle.xInc *= -1;
}
if (circle.y < 0 || circle.y > this.canvas.height) {
circle.yInc *= -1;
}
}
전체 소스코드
| main.ts
class DrawingApp {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private readonly circles: Circle[] = [];
private readonly colors: readonly string[] = ["red", "green", "blue"];
private colorsCount: number = 0;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.context = canvas.getContext("2d");
this.addCircles(10);
requestAnimationFrame(this.redraw);
}
private redraw = () => {
this.context.fillStyle = "white";
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
for (const circle of this.circles) {
this.drawCircle(circle);
this.moveCircle(circle);
}
requestAnimationFrame(this.redraw);
}
private drawCircle(circle: Circle) {
this.context.fillStyle = circle.color;
this.context.beginPath();
this.context.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI);
this.context.fill();
}
private getColor(): string {
let result = this.colors[this.colorsCount];
this.colorsCount = (this.colorsCount + 1) % this.colors.length;
return result;
}
private moveCircle(circle: Circle) {
circle.x += circle.xInc;
circle.y += circle.yInc;
if (circle.x < 0 || circle.x > this.canvas.width) {
circle.xInc *= -1;
}
if (circle.y < 0 || circle.y > this.canvas.height) {
circle.yInc *= -1;
}
}
public addCircles = (numbers: number) => {
var color = this.getColor();
for (var i = 0; i < numbers; i++) {
this.circles.push({
x: this.canvas.width * Math.random(),
y: this.canvas.height * Math.random(),
radius: 32 * Math.random() + 8,
xInc: 5 * Math.random() + 1,
yInc: 5 * Math.random() + 1,
color: color
});
}
}
}
type Circle = {
x: number,
y: number,
readonly radius: number,
readonly color: string,
xInc: number,
yInc: number,
};
다음으로 버튼을 눌렀을 때 원을 추가할 수 있도록 index.html
을 다음과 같이 수정합니다.
| index.html
<html>
<head>
<title>Canvas Sample</title>
</head>
<body>
<canvas id="canvas" width="800" height="800"
style="border: 1px solid black;">
</canvas>
<p></p>
<button id="add">add 10 circles</button>
<script src="main.js"></script>
<script type="text/javascript">
const app = new DrawingApp(document.getElementById('canvas'));
const button = document.getElementById('add');
button.onclick = () => app.addCircles(10);
</script>
</body>
</html>
실행 결과
tsc
에 의해 변환된 JavaScript 코드
컴파일된 JavaScript 코드를 살펴보는 것은 흥미롭니다. 다음은 컴파일된 JavaScript 코드입니다.
var DrawingApp = /** @class */ (function () {
function DrawingApp(canvas) {
var _this = this;
this.circles = [];
this.colors = ["red", "green", "blue"];
this.colorsCount = 0;
this.redraw = function () {
_this.context.fillStyle = "white";
_this.context.fillRect(0, 0, _this.canvas.width, _this.canvas.height);
for (var _i = 0, _a = _this.circles; _i < _a.length; _i++) {
var circle = _a[_i];
_this.drawCircle(circle);
_this.moveCircle(circle);
}
requestAnimationFrame(_this.redraw);
};
this.addCircles = function (numbers) {
var color = _this.getColor();
for (var i = 0; i < numbers; i++) {
_this.circles.push({
x: _this.canvas.width * Math.random(),
y: _this.canvas.height * Math.random(),
radius: 32 * Math.random() + 8,
xInc: 5 * Math.random() + 1,
yInc: 5 * Math.random() + 1,
color: color
});
}
};
this.canvas = canvas;
this.context = canvas.getContext("2d");
this.addCircles(10);
requestAnimationFrame(this.redraw);
}
DrawingApp.prototype.drawCircle = function (circle) {
this.context.fillStyle = circle.color;
this.context.beginPath();
this.context.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI);
this.context.fill();
};
DrawingApp.prototype.getColor = function () {
var result = this.colors[this.colorsCount];
this.colorsCount = (this.colorsCount + 1) % this.colors.length;
return result;
};
DrawingApp.prototype.moveCircle = function (circle) {
circle.x += circle.xInc;
circle.y += circle.yInc;
if (circle.x < 0 || circle.x > this.canvas.width) {
circle.xInc *= -1;
}
if (circle.y < 0 || circle.y > this.canvas.height) {
circle.yInc *= -1;
}
};
return DrawingApp;
}());
//# sourceMappingURL=main.js.map
정리
VS Code에 TypeScript 개발환경을 구성해서 코딩 결과를 웹브라우저를 통해 확인할 수 있도록 예제를 통해 확인해 보면서 TypeScript의 강력한 타입 검사 기능과 VS Code를 이용한 편리한 디버깅 환경을 살펴볼 수 있었습니다.