Javascript와 canvas를 이용해서 심플한 슈팅 게임을 만들어 볼까 한다.

캔버스에 키입력과 마우스 클릭, 마우스 이동 이벤트를 모두 받아서 처리했고,

주기적으로 업데이트를 해서 객체를 이동, 충돌 체크(거리 계산)를 하고

화면을 업데이트해서 게임이 진행되도록 했다.

게임해보기 → JS Shooting Game (sparrow-lee.github.io)

 

JS Shooting Game

 

sparrow-lee.github.io

 

 

 

 

 

 

  • 유저의 유닛은 마우스 포인터를 향해 조준을 하도록 함
  • 랜덤의 시간이 지날 때마다 적 유닛이 생성되고 (최대 20개), 랜덤의 시간마다 적 유닛은 유저 유닛을 향해 미사일을 쏘도록 함
  • 적 미사일을 유저 유닛이 맞을 때마다 에너지가 감소하고 0%가 되면 게임 종료
  • 적 유닛을 격추할 때 마다 score 1점 증가

 

body 영역은 canvas tag 하나만 존재한다

 

<body onload="onload()">
    <canvas id="canvas" style="background-color:rgb(185, 185, 185);" width="1000" height="600"></canvas>
</body>

 

 

 

초기화 함수에서는 canvas와 context는 전역으로 유지. 플레이어 초기화, 이벤트 핸들러 등록, 적 유닛 초기화.

모든 유닛(플레이어, 적 유닛, 미사일 등)을 업데이트 하는 타이머 등록

→ 유닛들을 이동시키고, 미사일과 유닛간에 충돌 여부 판단등을 하게 된다.

화면 갱신 함수(draw) 등록

 

 

        function onload() {
            console.log('> onload')
            gCanvas = document.getElementById('canvas');
            gContext = gCanvas.getContext("2d");

            let { width, height } = gCanvas;
            gCanvasWidth = width;
            gCanvasHeight = height;
            initPlayer();
            initEvent();
            initEnemy();
            gUpdateIntervalId = setInterval(updateUnits, UNIT_UPDATE_INTERVAL);
            window.requestAnimationFrame(draw);
        }

 

 

 

 

W,A,S,D 또는 화살표 커서로 플레이어 유닛을 이동시키게 하기 위해서, 키 업/다운 이벤트를 등록했다.

그냥 keypressed 이벤트에 등록을 할 수 도 있겠지만, 키를 매번 눌럿다 뗏다를 해야만 이동이 되는 문제가 있어서,

키를 눌렀을 때, keydown으로 상태를 저장해 놓고, 키를 뗏을 때는 다시 keyup 상태로 저장을 해두게 하고, 타이머로, 키 상태를 주기적으로 체크해서 눌려진 상태이면 계속 이동을 하게 했다.

→ 동시에 좌,우키가 둘다 눌려졌을 때도 처리를 한다. 좌표를 +,- 각각 해주어서 결국 제자리가 되게 함.

 

 

        function initEvent() {
            console.log('> initEvent');
            document.addEventListener('keydown', eventKeydown);
            document.addEventListener('keyup', eventKeyup);
            gCanvas.addEventListener('mousedown', eventMousedown);
            gCanvas.addEventListener('mousemove', eventMousemove);
        }

        function initPlayer() {
            console.log('> initPlayer');
            gPlayer = {
                x:50, y:50, size:PLAYER_SIZE, energy:PLAYER_ENERGY, speed:PLAYER_SPEED, score: 0, alive: true,
            }
            for (let i = 0; i < 20; i += 1) {
                gPlayerMissiles.push(new Missile({index:i, speed: PLAYER_MISSILE_SPEED}));
            }
        }

        function initEnemy() {
            console.log('> initEnemy');
            for (let i = 0; i < MAX_ENEMY_MISSILE; i += 1) {
                gEnemyMissiles.push(new Missile({index:i, speed: ENEMY_MISSILE_SPEED}));
            }
            for (let i = 0; i < MAX_ENEMY; i += 1) {
                gEnemy.push(new Enemy({index:i, speed: ENEMY_SPEED}));
            }
            for (let i = 0; i < MAX_ENEMY; i += 1) {
                let params = {
                    x: Math.random() * gCanvasWidth / 2 + (gCanvasWidth / 2), 
                    y: Math.random() * gCanvasHeight / 2 + (gCanvasHeight / 2),
                    size: ENEMY_SIZE, 
                    speed: ENEMY_SPEED,
                    worldW: gCanvasWidth,
                    worldH: gCanvasHeight
                }
                gEnemy[i].launch(params);
            }
        }

 

마우스가 움직일 때는 유저 유닛의 조준 상태를 마우스 위치로 업데이트 되도록 했다. 마우스의 최종 위치를 전역 변수에 업데이트 시킨 뒤에, 유저 유닛의 조준 각도를 계산하도록 했다.

 

 

        function eventMousemove(event) {
            const x = event.clientX - gCanvas.offsetLeft;
            const y = event.clientY - gCanvas.offsetTop;

            gPlayer.lastMouseX = x;
            gPlayer.lastMouseY = y;

            updateGunPos();
        }

        function updateGunPos() {
            let dx = Math.abs(gPlayer.x - gPlayer.lastMouseX);
            let dy = Math.abs(gPlayer.y - gPlayer.lastMouseY);
            let distance = Math.sqrt((dx * dx) + (dy * dy));
            
            gPlayer.gunX = (dx * gGunSize / distance) * (gPlayer.lastMouseX > gPlayer.x ? 1 : -1);
            gPlayer.gunY = (dy * gGunSize / distance) * (gPlayer.lastMouseY > gPlayer.y ? 1 : -1);
            // console.log(`mouse move gPlayer.gunX=${gPlayer.gunX}, gPlayer.gunY=${gPlayer.gunY}`);
        }

 

 

 

마우스 클릭을 하면, 유저의 유닛의 좌표에서 마우스 좌표를 향해서 미사일을 발사 한다.

x,y 방향 속도를 계산해서 가지고 있고, 타이머로 유닛 업데이트시에 그 속도값을 미사일 좌표에 더하기를 하게된다.

적 유닛 중 하나와 부딛히거나, 화면 밖으로 나가면 destroy 된다. 한번에 20개까지 쏠 수 있도록 클래스 인스턴스를 배열로 20개를 준비해 놓게 해두었다.

 

        function eventMousedown(e) {
            if (!gPlayer.alive) return;
            const x = event.clientX - gCanvas.offsetLeft;
            const y = event.clientY - gCanvas.offsetTop;
            // console.log(`eventMousedown click x=${x}, y=${y}`);

            let missile = gPlayerMissiles.find(m => {
                if (!m.alive) return true;
            })
            if (missile) {
                // console.log('eventMousedown. missile id = ', missile.index);

                const speed = missile.speed;
                let dx = Math.abs(gPlayer.x - gPlayer.lastMouseX);
                let dy = Math.abs(gPlayer.y - gPlayer.lastMouseY);
                let distance = Math.sqrt((dx * dx) + (dy * dy));

                let vx = (dx * speed / distance) * (gPlayer.lastMouseX > gPlayer.x ? 1 : -1);
                let vy = (dy * speed / distance) * (gPlayer.lastMouseY > gPlayer.y ? 1 : -1);

                missile.launch({x: gPlayer.x, y: gPlayer.y, size: PLAYER_MISSILE_SIZE, vx, vy})
            } else {
                console.log('no missile');
            }
        }

 

 

 

updateUnits라는 함수는 15ms마다 반복해서 호출되며, 유닛의 이동 처리, 충돌 계산 등을 하게 된다.

 

        function updateUnits() {
            // console.log('> updateUnits')
            // for player
            let speed = gPlayer.speed + (gKeyStatus.shiftKey ? 3 : 0);
            let mvX = 0; mvY = 0;
            if (gKeyStatus.left) mvX -= speed;
            if (gKeyStatus.right) mvX += speed;
            if (gKeyStatus.up) mvY -= speed;
            if (gKeyStatus.down) mvY += speed;

            if (mvX != 0 && mvY != 0) {
                mvX = mvX / Math.sqrt(2);
                mvY = mvY / Math.sqrt(2);
            }
            gPlayer.x += mvX;
            gPlayer.y += mvY;

            if (gPlayer.x < 0) gPlayer.x = 0;
            else if (gPlayer.x > gCanvasWidth) gPlayer.x = gCanvasWidth-1;
            if (gPlayer.y < 0) gPlayer.y = 0;
            else if (gPlayer.y > gCanvasHeight) gPlayer.y = gCanvasHeight-1;
            //console.log(`x=${gPlayer.x}, y=${gPlayer.y}`);
            updateGunPos();

            // for player's Missiles
            gPlayerMissiles.forEach(missile => {
                if (missile.alive) {
                    let {x, y} = missile.move();
                    if (x < 0 || y < 0) missile.destroy();
                    else if (x > gCanvasWidth || y > gCanvasHeight) missile.destroy();
                }
            })

            // collision check
            gPlayerMissiles.forEach(missile => {
                if (missile.alive) {
                    let {x:mx, y:my} = missile.pos;
                    
                    gEnemy.some(enemy => {
                        if (enemy.alive) {
                            let {x:ex, y:ey} = enemy.pos;
                            let checkDist = missile.size + enemy.size;
                            checkDist = checkDist * checkDist;
                            // console.log(`checkDist = ${checkDist}`);
                            // console.log(`dist = ` + ((ex - mx) * (ex - mx)) + ((ey - my) * (ey - my)));
                            if (((ex - mx) * (ex - mx)) + ((ey - my) * (ey - my)) < checkDist) {
                                // collision occurred.
                                missile.destroy();
                                enemy.destroy();
                                gPlayer.score += 1;
                                console.log(`gPlayer.score = ${gPlayer.score}`);
                                return true;
                            }
                        }
                    })
                }
            })

            // for enemy's Missiles move
            gEnemyMissiles.forEach(missile => {
                if (missile.alive) {
                    let {x, y} = missile.move();
                    if (x < 0 || y < 0) missile.destroy();
                    else if (x > gCanvasWidth || y > gCanvasHeight) missile.destroy();
                }
            })
            // collision check
            gEnemyMissiles.forEach(missile => {
                if (missile.alive) {
                    let {x:mx, y:my} = missile.pos;

                    if (gPlayer.alive) {
                        let {x:px, y:py} = gPlayer;
                        let checkDist = missile.size + gPlayer.size;
                        checkDist = checkDist * checkDist;
                        // console.log(`checkDist = ${checkDist}`);
                        // console.log(`dist = ` + ((ex - mx) * (ex - mx)) + ((ey - my) * (ey - my)));
                        if (((px - mx) * (px - mx)) + ((py - my) * (py - my)) < checkDist) {
                            // collision occurred.
                            missile.destroy();
                            gPlayer.energy -= 1;
                            console.log(`gPlayer.energy = ${gPlayer.energy}`);
                            if (gPlayer.energy <= 0) {
                                gPlayer.energy = 0;
                                // destroy
                                console.log('destroyed. game over !!!')
                                gPlayer.alive = false;
                            }
                            return true;
                        }
                    }

                }
            })

            // enemy 이동
            let aliveEnemy = 0;
            gEnemy.forEach(enemy => {
                if (enemy.alive) {
                    aliveEnemy += 1;
                    enemy.move();
                }
            })

 ... 길어서 잘림

 

 

 

미사일과 적 유닛은 class로 구현을 했다.

 

        class MovingObject {
            constructor({index, speed}) {
                this.x = 0;
                this.y = 0;
                this.size = 0;
                this.vx = 0;
                this.vy = 0;
                this.speed = speed;
                this.index = index;
                this.active = false;
            }
            destroy() {
                this.active = false;
            }
            get pos() {
                return {x: this.x, y: this.y};
            }
            get alive() {
                return this.active;
            }
            setAlive(bAlive) {
                this.active = bAlive;
            }
        }
        class Missile extends MovingObject {
            constructor(...args) {
                super(...args);
                this.energy = 0;
            }
            launch({x, y, size, vx, vy}) {
                this.x = x;
                this.y = y;
                this.size = size;
                this.vx = vx;
                this.vy = vy;
                this.active = true;
                this.energy = 1;
            }
            move() {
                if (this.active) {
                    this.x = this.x + this.vx;
                    this.y = this.y + this.vy;
                    return {x: this.x, y: this.y};
                } else {
                    return {x: -1, y: -1};
                }
            }
        }
        class Enemy extends MovingObject {
            constructor(...args) {
                super(...args);
            }
            launch({x, y, size, speed, worldW, worldH}) {
                // console.log(`launch x=${x}, y=${y}, worldW=${worldW}, worldH=${worldH}`)
                this.x = x;
                this.y = y;
                this.size = size;
                this.speed = speed;
                this.vx = 0;
                this.vy = 0;
                this.active = true;
                this.worldW = worldW;
                this.worldH = worldH;

                this.moveCount = 0;
                this.setNextSpeed(Math.random() * worldW, Math.random() * worldH);
            }
            move() {
                if (this.active) {
                    this.x = this.x + this.vx;
                    this.y = this.y + this.vy;
                    this.moveCount -= 1;
                    if (this.moveCount < 0) {
                        this.setNextSpeed(Math.random() * this.worldW, Math.random() * this.worldH);
                    }
                    return {x: this.x, y: this.y};
                } else {
                    return {x: -1, y: -1};
                }
            }
            setNextSpeed(tx, ty) {
                // console.log(`setNextSpeed tx=${tx}, ty=${ty}`);
                let dx = Math.abs(this.x - tx);
                let dy = Math.abs(this.y - ty);
                let distance = Math.sqrt((dx * dx) + (dy * dy));
                
                this.vx = (dx * this.speed / distance) * (tx > this.x ? 1 : -1);
                this.vy = (dy * this.speed / distance) * (ty > this.y ? 1 : -1);                
                this.moveCount = Math.abs(dx / this.vx);
                // console.log(`setNextSpeed this.vx = ${this.vx}, this.vy = ${this.vy}, this.moveCount=${this.moveCount}`)
            }
        }

 

 

화면 업데이트는 draw 함수에서 수행되는데,

 

window.requestAnimationFrame(draw);

 

 

draw 마지막에 호출해주면, 화면 업데이트 후 준비가 되는대로 다시 draw를 호출되게 자동으로 간격도 조절을 한다고 한다. 대략 1초에 60번이라고 본 것 같다.

즉, 이 게임에서는 2개의 interval 펑션이 동작하고 있다고 보면 된다. 화면을 그리는 draw함수와, 유닛 처리를 하는 updateUnits 함수.

draw에서는 유저유닛, 미사일들, 적유닛 등을 그려준다. 유저유닛과 미사일은 파란색, 적유닛과 미사일은 붉은색 계열로 그리고, 하단에 현재 energy와 score도 표시하게 했다.

 

 

        function draw() {
            // console.log('> draw');
            gContext.clearRect(0, 0, gCanvas.width, gCanvas.height);
            
            gContext.fillStyle = 'blue';
            gContext.strokeStyle = 'black';
            gContext.beginPath();
            gContext.arc(gPlayer.x, gPlayer.y, gPlayer.size, 0, 2 * Math.PI);
            gContext.fill();
            gContext.moveTo(gPlayer.x, gPlayer.y);
            gContext.lineTo(gPlayer.x + gPlayer.gunX, gPlayer.y + gPlayer.gunY);
            gContext.stroke();
            
            gContext.strokeStyle = 'blue';
            gPlayerMissiles.forEach(missile => {
                if (missile.alive) {
                    gContext.beginPath();
                    gContext.arc(missile.x, missile.y, missile.size, 0, 2 * Math.PI);
                    gContext.stroke();
                }
            });
            gContext.fillStyle = 'orange';
            gContext.strokeStyle = 'black';
            gEnemy.forEach(enemy => {
                if (enemy.alive) {
                    gContext.beginPath();
                    gContext.arc(enemy.x, enemy.y, enemy.size, 0, 2 * Math.PI);
                    gContext.stroke();
                    gContext.fill();
                }
            });
            gContext.strokeStyle = 'red';
            gEnemyMissiles.forEach(missile => {
                if (missile.alive) {
                    gContext.beginPath();
                    gContext.arc(missile.x, missile.y, missile.size, 0, 2 * Math.PI);
                    gContext.stroke();
                }
            });

            // draw energy area
            if (gPlayer.alive) {
                gContext.fillStyle = 'red';
                gContext.fillRect(45, gCanvasHeight - 40, 100 * (gPlayer.energy / PLAYER_ENERGY), 20);
                gContext.strokeStyle = 'yellow';
                gContext.strokeRect(45, gCanvasHeight - 40, 100, 20);

                gContext.fillStyle = 'black';
                gContext.font = "16px arial";
                gContext.fillText(`E`, 30, gCanvasHeight - 25);
                gContext.fillText(`${parseInt((gPlayer.energy / PLAYER_ENERGY) * 100)}%`, 77, gCanvasHeight - 25);
            } else {
                gContext.fillStyle = 'red';
                gContext.font = "20px arial";
                gContext.fillText(`Game Over !`, 30, gCanvasHeight - 25);
            }

            gContext.fillStyle = 'black';
            gContext.font = "16px arial";
            gContext.fillText(`Score ${gPlayer.score}`, 160, gCanvasHeight - 25);
            

            window.requestAnimationFrame(draw);
        }

 

 

 

나름 디테일에도 신경을 썼는데,

예를 들어, 스피드가 10이라고 하면, 오른쪽 방향키를 누르고 있다면 x좌표에 10을 계속 더하고, 아래쪽 방향키를 누르고 있으면 y좌표에 10을 더하게 된다.

그런데, 오른쪽키와 아래쪽 방향키를 동시에 누르고 있으면 x도 10증가, y도 10증가를 하게되어 사실 루트2배 만큼 더 움직인 꼴이 된다. 그럴 경우에는 x, y 각각 루트2로 나눈값으로 조절하여 이동 거리가 동일하도록 계산했다.

→ 두변의 길이가 1인 직각 삼각형의 빗변의 길이는 루트2 = sqrt(2) 이므로, 반대로 빗변의 길이가 속도 10일 때는 루트2로 나누면 두 빗변의 길이가 되겠다.

그리고, 쉬프트키를 누르고 있을 때는 부스터로, 좀 더 빠르게 움직이도록 속도를 3 더 증가 시켰다.

 

 

        function updateUnits() {
            // console.log('> updateUnits')
            // for player
            let speed = gPlayer.speed + (gKeyStatus.shiftKey ? 3 : 0);
            let mvX = 0; mvY = 0;
            if (gKeyStatus.left) mvX -= speed;
            if (gKeyStatus.right) mvX += speed;
            if (gKeyStatus.up) mvY -= speed;
            if (gKeyStatus.down) mvY += speed;

            if (mvX != 0 && mvY != 0) {
                mvX = mvX / Math.sqrt(2);
                mvY = mvY / Math.sqrt(2);
            }
            gPlayer.x += mvX;
            gPlayer.y += mvY;
​

 

 

 

전체 코드는 아래에서 볼 수 있다.

exam-codes-public/javascript/js-shooting-game.html at main · sparrow-lee/exam-codes-public (github.com)

 

 

 

Javascript에서 canvas라는 것을 이용하면 그림을 그려볼 수 가 있다. 이를 활용해서 게임 같은것도 만들수 있을 것 같다.

 

canvas tag를 이용해서 body에 정의를 해놓고

<body onload="onload()">
    <canvas id="canvas" style="background-color:rgb(185, 185, 185)" width="800" height="400"></canvas>
</body>

 

 

javascript 코드 제어가 가능하다. context 객체를 가져와서 거기에 그려주면 된다.

    let canvas = document.getElementById('canvas');
    let context = gCanvas.getContext("2d");
    const { width, height } = canvas;

 

beginPath로 시작을 하고, arc, line, rect 등을 호출하면 원, 라인, 네모 등의 영역을 그린 후

fill을 불러서 채우거나, stroke를 호출해서 선을 그리게 한다.

 

	let x1 = 100, y1 = 100, x2 = 200, y2 = 200;
	let x = 300, y = 300, size = 50;
	
	context.clearRect(0, 0, canvas.width, canvas.height);

	context.fillStyle = 'white';
	context.strokeStyle = "blue";

	context.beginPath();
	context.arc(x, y, size, 0, 2 * Math.PI);	
	context.fill();
	context.stroke();

	context.beginPath();
	context.moveTo(x1, y1);
	context.lineTo(x2, y2);
	context.stroke();

 

 

window.requestAnimationFrame(draw);

 

 

를 사용하면 된다. draw는 context로 그림을 그리는 함수로 유저가 작성해야된다.

이 함수를 이용하면, 그림 그리기가 준비가 되었을 때 화면업데이트를 해주게 되며, (아마도 draw함수가 다 수행되었을 때일 듯 싶다) 1초에 60번까지 업데이트하며, 필요에 따라 조절이 된다고 한다.

setInterval 이나 setTimeout으로 직접 반복 업데이트를 해도 된다고 한다.

여기에 더 재밌는 예시들이 있다.

 

Basic animations - Web APIs | MDN (mozilla.org)

 

Basic animations - Web APIs | MDN

Since we're using JavaScript to control <canvas> elements, it's also very easy to make (interactive) animations. In this chapter we will take a look at how to do some basic animations.

developer.mozilla.org

 

캔버스를 이용해서 간단한 게임을 한번 만들어 볼까 한다.

 

 

◆ HTML, Javascript, Plotly 등을 이용하여 아래 그림처럼 나만의 비트코인 차트를 만들어 보자

 

 

* 차트 실행해보기 → Bitcoin Chart (sparrow-lee.github.io)

 

Bitcoin Chart

캔들 단위 1분 3분 5분 10분 15분 30분 1시간 4시간 업데이트 간격 없음 1초 3초 10초 30초 1분 5분 30분 1시간

sparrow-lee.github.io

 

onLoad 초기화 함수에서는

  • 업비트에서 전체 마켓 코인 리스트를 받아와서 전역변수로 가지고 있고, (필요한 것만 재가공해서) Select태그 내부에 옵션 태그를 동적으로 추가시켜서 코인 리스트를 선택 가능하게 한다.
  • 그리고, 이전에 저장했던 셋팅값들을 localStorage에서 읽어와서, 캔들 시간, 선택 코인들을 셋팅한 다음
  • 차트를 그리게 한다.
            console.log('> init')
            await getAllCoinInfo();
            console.log('getAllCoinInfo done');
            setSelectTags()
            console.log('set select list');

            // 저장된 캔들 시간 유닛 읽어오기
            gSelectedUnitIndex = getData("candleUnit");
            if (gSelectedUnitIndex == undefined) {
                console.log('gSelectedUnitIndex = undefined');
                gSelectedUnitIndex = 7;   // 디폴드 4시간봉
                setData("candleUnit", 7);
            }
            let selectUnitElement = document.getElementById("selectUnit");
            selectUnitElement.selectedIndex = gSelectedUnitIndex;
            gSelectedUnitValue = selectUnitElement.options[selectUnitElement.selectedIndex].value;

            console.log("selectUnitElement.options[e.selectedIndex] : " + gSelectedUnitValue);

            // 유저가 선택했던 코인 리스트 읽어오기
            let savedValue = getData("markets");
            console.log(`getData market=${savedValue}`);
            if (savedValue && savedValue.length > 3) {
                let markets = savedValue.split(',');
                
                gSelectedCoins = [];              
                markets.forEach(market => {
                    if (market != 'None') {
                        let info = gAllCoinsInfo.KRWDict[market];
                        gSelectedCoins.push({
                            market,
                            name : info.name
                        })
                    }
                })
            } else {
                console.log('no saved data form markets')
            }
            let idx = 0;
            gSelectedCoins.forEach(coin => {
                setSelectMarket(idx, gAllCoinsInfo.KRWDict[coin.market].index + 1)

                idx += 1;
            })
            console.log(JSON.stringify(gSelectedCoins));
            console.log('> init done')

 

 

 

setInterval 을 이용하여 주기적으로 업데이트 되도록 함.

        // 업데이트 간격 변경시
        function updateIntervalOnChange(index, unit, text) {
            console.log(`> updateIntervalOnChange > index=${index}, unit value=${unit}, text=${text}`);

            if (gIntervalId) {
                clearInterval(gIntervalId);
                gInterval = 0;
            }
            if (unit == 'None') {
                console.log('update interval = 0');
                gInterval = 0;
                return;
            }
            gInterval = gUpdateInterval[unit];
            startInterval();
        }

        // 반복 시작
        function startInterval() {
            console.log("> startInterval");
            if (gInterval && gInterval > 0) {
                gIntervalId = setInterval(intervalHandler, gInterval);
            }
        }

        // 반복 처리 함수. 전체 코인 시세를 가져와서 다시 그리기
        async function intervalHandler() {
            console.log("> intervalHandler");
            await redrawAll();
        }

 

 

→ setTimeout 은 한번만 수행되는 것이고, setInterval은 주기적으로 계속 반복을 하게 된다. 멈춰야 할 경우는 clearInterval을 호출하면 된다. setInterval 호출시 반환되는 intervalId를 저장해두었다가 clearInterval할 때 같이 넘겨주면 된다.

코인을 변경하였을 때 처리.

localStorage에 선택사항을 저장하고 차트를 그려준다. 6개의 차트가 있고 id가 각각 0~5번까지 있다.

None을 선택하면, div 를 hidden 처리한다.

 

 

        async function selectOnChange(selId, value, text) {
            console.log(`> selectOnChange${selId} : ${value}, ${text}`);
            gSelectedCoins[selId] = {
                market: value,
                name: text,
            };

            let chartDivElement = document.getElementById(`chart${selId}`);
            if (value == 'None') {
                chartDivElement.hidden = 'hidden';
            } else {
                chartDivElement.hidden = undefined;
            }

            let markets = [];
            gSelectedCoins.forEach(coin => {
                if (coin.market != 'None') markets.push(coin.market);
            })

            console.log(markets.join(','));
            setData('markets', markets.join(','));

            if (value != 'None') {
                let res = await getBitPrice({count: 100, market: value, unit: gSelectedUnitValue, name: text});
                drawChart(res.trace, res.info, `chart${selId}`);
            }
        }

 

 

 

6개의 select와 6개의 div (차트영역)

 

    <select name="selectMarket0" id="selectMarket0" class="form-select form-select-sm select_coin" onchange="selectOnChange(0, this.value, this.options[this.selectedIndex].text)"></select>
    <select name="selectMarket1" id="selectMarket1" class="form-select form-select-sm select_coin" onchange="selectOnChange(1, this.value, this.options[this.selectedIndex].text)"></select>
    <select name="selectMarket2" id="selectMarket2" class="form-select form-select-sm select_coin" onchange="selectOnChange(2, this.value, this.options[this.selectedIndex].text)"></select>
    <select name="selectMarket3" id="selectMarket3" class="form-select form-select-sm select_coin" onchange="selectOnChange(3, this.value, this.options[this.selectedIndex].text)"></select>
    <select name="selectMarket4" id="selectMarket4" class="form-select form-select-sm select_coin" onchange="selectOnChange(4, this.value, this.options[this.selectedIndex].text)"></select>
    <select name="selectMarket5" id="selectMarket5" class="form-select form-select-sm select_coin" onchange="selectOnChange(5, this.value, this.options[this.selectedIndex].text)"></select>
    <br/><br/>
    <div id="chart0" style="width:100%;height:200px;float: both" hidden></div>
    <div id="chart1" style="width:100%;height:200px;float: both" hidden></div>
    <div id="chart2" style="width:100%;height:200px;float: both" hidden></div>
    <div id="chart3" style="width:100%;height:200px;float: both" hidden></div>
    <div id="chart4" style="width:100%;height:200px;float: both" hidden></div>
    <div id="chart5" style="width:100%;height:200px;float: both" hidden></div>

 

 

캔들 단위와 업데이트 간격 select 태그

 

    <table style="width:50%; min-width:300px"><tr><td>
    <div style="float: left; font-size: 15px; margin:4px"> 캔들 단위 </div>
    <select name="selectUnit" id="selectUnit" class="form-select form-select-sm" style="width:20%; min-width:100px; vertical-align:middle" onchange="selectUnitOnChange(this.selectedIndex, this.value, this.options[this.selectedIndex].text)">
        <option id="0" value="1">1분</option>
        <option id="1" value="3">3분</option>
        <option id="2" value="5">5분</option>
        <option id="3" value="10">10분</option>
        <option id="4" value="15">15분</option>
        <option id="5" value="30">30분</option>
        <option id="6" value="60">1시간</option>
        <option id="7" value="240">4시간</option>
    </select>
    </td><td>
    <div style="float: left; font-size: 15px; margin:4px"> 업데이트 간격 </div>
    <select name="updateInterval" id="updateInterval" class="form-select form-select-sm" style="width:20%; min-width:100px; vertical-align:middle" onchange="updateIntervalOnChange(this.selectedIndex, this.value, this.options[this.selectedIndex].text)">
        <option id="0" value="None">없음</option>
        <option id="1" value="1s">1초</option>
        <option id="2" value="3s">3초</option>
        <option id="3" value="10s">10초</option>
        <option id="4" value="30s">30초</option>
        <option id="5" value="1m">1분</option>
        <option id="6" value="5m">5분</option>
        <option id="7" value="30m">30분</option>
        <option id="8" value="1h">1시간</option>
    </select>
    </td></tr></table>

 

 

 

전체 코인정보를 업비트로부터 읽어온다. 별도의 개발자 등록이나 키값이 필요하진 않다. 개인 거래이력까지 가져올려면 키를 받아오긴 해야된다.

전체 정보를 읽어와서 접근하기 쉬운형태로 재가공해서 전역변수로 가지고 있게한다.

 

 

        // 전체 코인 메타 정보 가져오기
        async function getAllCoinInfo() {
            let url = `https://api.upbit.com/v1/market/all`;
            const response = await fetch(url);
            const coinInfos = await response.json();

            // 코인 검색시 JSON 형태가 쉬울때가 많아서 리스트와 함께 두가지 형태로 저장한다
            gAllCoinsInfo = {
                "KRW" : [],
                "KRWDict" : {},
            };

            let KRW = [], head = [];
            let KRWDict = {};

            coinInfos.forEach(info => {
                let ids = info.market.split("-");

                if (ids[0] == 'KRW') {
                    let name = info.korean_name;
                    let en_name = info.english_name;

                    // 비트코인과 이더리움은 정렬에서 제외시키고 맨앞에 둔다.
                    if (ids[1] == 'BTC' || ids[1] == 'ETH') {
                        head.push({
                            market : info.market, name, en_name,
                        })
                    } else {
                        KRW.push({
                            market : info.market, name, en_name,
                        })
                    }
                    
                }
            })
            KRW = KRW.sort((a, b) => a.name > b.name ? 1 : -1);

            gAllCoinsInfo.KRW = head.concat(KRW);

            for(let i=0; i<gAllCoinsInfo.KRW.length; i += 1) {
                let coin = gAllCoinsInfo.KRW[i];
                gAllCoinsInfo.KRWDict[coin.market] = {
                    index: i,
                    market: coin.market,
                    name: coin.name,
                    en_name: coin.en_name,
                }
            }
            console.log(gAllCoinsInfo)
        }

 

 

아무래도 코인 market id (예. "KRW-BTC") 로 검색을 하는 경우가 많아 JSON 형태로 데이터(gAllCoinsInfo.KRWDict)를 별도로 가지고 있게 했다. select 옵션에 넣을 때는 list형태가 편하므로 list(gAllCoinsInfo.KRW) 로도 가지고 있게했다.

특정 코인, 캔들 간격으로 시세 가져오기. 코인이 바뀔 수 있게 parameter로 넘기게 구현

        // 특정 코인 캔들 정보 리스트 가져오기
        async function getBitPrice({count, market, unit, name}) {
            let url = `https://api.upbit.com/v1/candles/minutes/${unit}?market=${market}&count=${count}`;
            const response = await fetch(url);
            const prices = await response.json();

 

 

코인이 동시에 6개에, 캔들 간격, 업데이트 간격 등이 들어가게 되어서 코드가 제법 복잡해졌다.

서버 없이도 나름 쓸만한 로컬 동작가능한 차트 뷰어가 만들어 진 것 같다.

 

 

소스 코드는 아래를 참고하면 된다.

 

exam-codes-public/javascript/bitcoin-chart.html at main · sparrow-lee/exam-codes-public (github.com)

직접 HTML로 UI를 구성할 때, 기본 widget들을 이용하는 것 보다, bootstrap 같은 UI 툴킷 라이브러리를 쓰면 화면을 좀 더 깔끔하게 구성할 수 있다.

 

* example 보기 → Bootstrap Exam (sparrow-lee.github.io)

 

Bootstrap Exam

Hello, world! This is a toast message.

sparrow-lee.github.io

 

리액트 같은 프론트엔드를 설치하지 않고, HTML로 직접 구성을 할 때, CDN link 방식으로 라이브러리를 가져다 쓸 수 있다.

    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js" integrity="sha384-BBtl+eGJRgqQAUMxJ7pMwbEyER4l1g+O15P+16Ep7Q9Q+zqX6gSbd85u4mG4QzX+" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"  integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">

 

 

맨위 예시 스크린샷의 html 코드는 아래 위치 참고

exam-codes-public/javascript/bootstrap.html at main · sparrow-lee/exam-codes-public (github.com)

 

 

 

공식 가이드는 여기 참고

Get started with Bootstrap · Bootstrap v5.3 (getbootstrap.com)

 

Get started with Bootstrap

Bootstrap is a powerful, feature-packed frontend toolkit. Build anything—from prototype to production—in minutes.

getbootstrap.com

 

 

 

이전에 올린 캔들 차트 그리기에 한가지 기능을 추가해보려고 한다.

window.localStorage를 이용하면 key, value 쌍으로 데이터를 저장할 수 있다.

이를 이용해서, 이전에 선택했던 값(코인 종류)을 저장해놓고, 다음에 브라우저를 열었을 때, 해당 선택값으로 초기화해서 다시 시작하는 것이다.

 

* LocalStorage 적용 버전 테스트해보기 → Local Storage exam (sparrow-lee.github.io)

 

Local Storage exam

 

sparrow-lee.github.io

 

브라우저 Javascript Candle Chart 그리기 :: 코딩하는 참새 (tistory.com)

 

브라우저 Javascript Candle Chart 그리기

HTML + Javascript를 이용하여 브라우저에 아래와 같은 캔들 차트를 그려보자. ​ * 차트 예시 실행해보기 → Draw chart exam (sparrow-lee.github.io) Draw chart exam sparrow-lee.github.io ​ 위 예시 데이터는 비트코인

codesparrow.tistory.com

 

 

이전 예시에서는 비트코인 한가지 차트를 보여줬지만, 콤보박스를 추가해서 다른 코인도 선택가능하게 하고,

선택된 코인을 localStorage에 저장해 놓고, 다음번에도 같은 코인을 보여주게 하는 것이다.

        function setData(key, value) {
            window.localStorage.setItem(key, value);
        }

        function getData(key) {
            let value = window.localStorage.getItem(key);
            return value;
        }

        // 콤보박스가 변경되었을 때, 선택된 코인 마켓id를 저장하고, 차트를 다시 그린다.
        async function selectMarketOnChange(index, value, text) {
            setData('market', value);
            selectedMarket = value;
            let unit = '240';
            let name = COIN_DICT[selectedMarket].name;
            let res = await getBitPrice({count: 100, market: selectedMarket, unit, name});
            drawChart(res.trace, res.info);
        }

 

 

예시에서는 7개의 코인만 static하게 추가해두긴 했는데, 업비트에서 제공하는 다른 Rest API를 이용하면

업비트에 상장된 전체 코인 리스트도 받아 올 수 있다. 그래서, 동적으로 콤보박스의 옵션을 추가하는 것도 가능하겠다.

 

 

        async function getAllCode() {
            let url = `https://api.upbit.com/v1/market/all`;
            const response = await fetch(url);
            const codes = await response.json();
            ...

 

 

localStorage에는 다른 기능도 더 있긴하다.

 

 
window.localStorage.removeItem('myKey'); // 해당 키값에 해당하는 key, value 삭제

window.localStorage.clear();  // 전체 삭제

 

 

다만, 주의할 것이, html 파일별로 localStorage가 분리되어 저장되는 것은 아니고, 브라우저별로 별도 저장되는 것으로 보이므로, 다른 html문서에서 저장한 localStorage의 key값과 중복되지 않도록 unique한 이름으로 하는 것이 좋아 보인다.

 

 

동적으로 콤보박스의 옵션을 추가하는 것은,

            // 선택 가능한 코인 리트스로 option 추가
            let selectMarketElement = document.getElementById("selectMarket");
            let option = document.createElement("option");

            for (let i = 0; i < COIN_LIST.length; i += 1) {
                option = document.createElement("option");
                option.text = COIN_LIST[i].name;
                option.value = COIN_LIST[i].market;
                selectMarketElement.appendChild(option);
            }

 

 

select tag로 아이템 추가. 선택 변경이 있을 때, selectMarketOnChange 함수가 불리게 했다.

<select name="selectMarket" id="selectMarket" onchange="selectMarketOnChange(this.selectedIndex, this.value, this.options[this.selectedIndex].text)">

 

 

콤보박스가 변경되었을 때 실행되는 함수. localStorage에 저장하고, 차트 다시 그리기

        async function selectMarketOnChange(index, value, text) {
            setData('market', value);
            selectedMarket = value;
            let unit = '240';
            let name = COIN_DICT[selectedMarket].name;
            let res = await getBitPrice({count: 100, market: selectedMarket, unit, name});
            drawChart(res.trace, res.info);
        }

 

 

 

아래는 전체 코드

 

<html lang="ko">
<head>
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
    <meta charset="utf-8" />
    <title>Local Storage exam</title>
    <script src="https://cdn.plot.ly/plotly-2.27.0.min.js" charset="utf-8"></script>
    <script>
        let COIN_LIST = [
            { idx: 0, market: "KRW-BTC", name: "비트코인"},
            { idx: 1, market: "KRW-ETH", name: "이더리움"},
            { idx: 2, market: "KRW-DOGE", name: "도지코인"},
            { idx: 3, market: "KRW-LSK", name: "리스크"},
            { idx: 4, market: "KRW-XRP", name: "리플"},
            { idx: 5, market: "KRW-SAND", name: "샌드박스"},
            { idx: 6, market: "KRW-SOL", name: "솔라나"},
        ];
        let COIN_DICT = {};
        let selectedMarket = '';
        
        async function onLoad() {
            console.log('onLoad')
            init();
            console.log(JSON.stringify(window.localStorage));
            // 선택 가능한 코인 리트스로 option 추가
            let selectMarketElement = document.getElementById("selectMarket");
            let option = document.createElement("option");

            for (let i = 0; i < COIN_LIST.length; i += 1) {
                option = document.createElement("option");
                option.text = COIN_LIST[i].name;
                option.value = COIN_LIST[i].market;
                selectMarketElement.appendChild(option);
            }

            // 이전에 선택했던 코인으로 선택된 상태로 설정
            let unit = '240';
            let name = COIN_DICT[selectedMarket].name;
            selectMarketElement.selectedIndex = COIN_DICT[selectedMarket].idx;

            // 차트 그리기
            let res = await getBitPrice({count: 100, market: selectedMarket, unit, name});
            drawChart(res.trace, res.info);
        }

        function init() {
            COIN_LIST.forEach(coin => {
                COIN_DICT[coin.market] = coin
            })
            console.log(COIN_DICT);
            selectedMarket = getData('market');
            if (!selectedMarket) {
                selectedMarket = 'KRW-BTC';
            }
            console.log('init done');
        }

        function setData(key, value) {
            window.localStorage.setItem(key, value);
        }

        function getData(key) {
            let value = window.localStorage.getItem(key);
            return value;
        }

        async function selectMarketOnChange(index, value, text) {
            setData('market', value);
            selectedMarket = value;
            let unit = '240';
            let name = COIN_DICT[selectedMarket].name;
            let res = await getBitPrice({count: 100, market: selectedMarket, unit, name});
            drawChart(res.trace, res.info);
        }

        async function getBitPrice({count, market, unit, name}) {

            let url = `https://api.upbit.com/v1/candles/minutes/${unit}?market=${market}&count=${count}`;
            const response = await fetch(url);
            const prices = await response.json();
            console.log(prices);

            let pricesSorted = prices.sort((a, b) => a.timestamp - b.timestamp);    // 시간순으로 정렬

            let x = [];
            let high = [];
            let low = [];
            let open = [];
            let close = [];

            let info = {};
            // 차트 그릴 때, 최소, 최대값 range를 설정하기 위해.
            lowest_price = pricesSorted[0].low_price;
            highest_price = pricesSorted[0].high_price;
            pricesSorted.forEach((p) => {
                x.push(p.candle_date_time_kst);
                high.push(p.high_price);
                low.push(p.low_price);
                open.push(p.opening_price);
                close.push(p.trade_price);

                lowest_price = lowest_price > p.low_price ? p.low_price : lowest_price;
                highest_price = highest_price < p.high_price ? p.high_price : highest_price;
            })

            info = {
                lowest_price,
                highest_price,
                market,
                name
            }

            // 차트 그릴 때 필요로 하는 정보들. 캔들값을 배열로.
            let trace = {
                x,
                high,
                low,
                open,
                close,
                decreasing: {line: {color: 'blue'}},
                increasing: {line: {color: 'red'}},
                line: {color: 'rgba(31,119,180,1)'},
                type: 'candlestick', 
                xaxis: 'x', 
                yaxis: 'y'
            }
            console.log(info);
            console.log('>> getBitPrice done')
            return {trace, info};
        }

 

 

        function drawChart(trace, info) {
            var data = [trace];
                
            var layout = {
                dragmode: 'zoom', 
                margin: {
                    r: 10, 
                    t: 25, 
                    b: 40, 
                    l: 60
                }, 
                showlegend: false, 
                xaxis: {
                    autorange: true, 
                    domain: [0, 1], 
                    //range: ['2017-01-03 12:00', '2017-02-15 12:00'], 
                    range: [trace.x[0], trace.x[trace.x.length - 1]],
                    rangeslider: {range: [trace.x[0], trace.x[trace.x.length - 1]]}, 
                    title: info.name + ' / ' + info.market, 
                    type: 'date',
                    rangeslider: {
                        visible: false
                    }
                }, 
                yaxis: {
                    autorange: true, 
                    domain: [0, 1], 
                    range: [info.lowest_price * 0.95, info.highest_price * 1.05], 
                    type: 'linear'
                }
            };
                
            Plotly.newPlot('chartDiv', data, layout);
            console.log('>> drawChart done')
        }
    </script>
</head>
<body onload="onLoad()">
    <select name="selectMarket" id="selectMarket" onchange="selectMarketOnChange(this.selectedIndex, this.value, this.options[this.selectedIndex].text)">
    </select>
    <div id="chartDiv" style="width:100%;height:500px;"></div>
</body>
</html>

 

 

Github 소스코드 참고

exam-codes-public/javascript/local-storage.html at main · sparrow-lee/exam-codes-public (github.com)

 

 

HTML + Javascript를 이용하여 브라우저에 아래와 같은 캔들 차트를 그려보자.

* 차트 예시 실행해보기 → Draw chart exam (sparrow-lee.github.io)

 

Draw chart exam

 

sparrow-lee.github.io

위 예시 데이터는 비트코인 4시간봉 데이터를 업비트에서 Rest API로 가져와서 그린 것으로 실시간 데이터를 가져온 것이다. 업비트에서 오픈으로 제공하고 있고 별도 auth나 secret key가 필요하진 않다.

 

분(Minute) 캔들 (upbit.com)

 

Open API | 업비트 개발자 센터

 

docs.upbit.com

 

    async function getBitPrice({count, market, unit, name}) {
        let url = `https://api.upbit.com/v1/candles/minutes/${unit}?market=${market}&count=${count}`;
        const response = await fetch(url);
        const prices = await response.json();

 

cdn에 올려서 제공하는 라이브러리 스크립트를 가져다 쓸 수 있다.

 

Plotly.newPlot('chartDiv', data, layout);
// javascript에서 데이터, 레이아웃 및 그려질 div id를 넘겨주면 그려준다

 

 

※ 아래는 전체 코드

 

<html lang="ko">
<head>
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
    <meta charset="utf-8" />
    <title>Draw chart exam</title>
    <script src="https://cdn.plot.ly/plotly-2.27.0.min.js" charset="utf-8"></script>
    <script>
        async function onLoad() {
            console.log('onLoad')
            let market = 'KRW-BTC';
            let unit = '240';
            let name = '비트코인';
            let res = await getBitPrice({count: 100, market, unit, name});
            drawChart(res.trace, res.info);
        }


        async function getBitPrice({count, market, unit, name}) {
            let url = `https://api.upbit.com/v1/candles/minutes/${unit}?market=${market}&count=${count}`;
            const response = await fetch(url);
            const prices = await response.json();
            console.log(prices);

            let pricesSorted = prices.sort((a, b) => a.timestamp - b.timestamp);    // 시간순으로 정렬

            let x = [];
            let high = [];
            let low = [];
            let open = [];
            let close = [];

            let info = {};
            // 차트 그릴 때, 최소, 최대값 range를 설정하기 위해.
            lowest_price = pricesSorted[0].low_price;
            highest_price = pricesSorted[0].high_price;
            pricesSorted.forEach((p) => {
                x.push(p.candle_date_time_kst);
                high.push(p.high_price);
                low.push(p.low_price);
                open.push(p.opening_price);
                close.push(p.trade_price);

                lowest_price = lowest_price > p.low_price ? p.low_price : lowest_price;
                highest_price = highest_price < p.high_price ? p.high_price : highest_price;
            })

            info = {
                lowest_price,
                highest_price,
                market,
                name
            }

            // 차트 그릴 때 필요로 하는 정보들. 캔들값을 배열로.
            let trace = {
                x,
                high,
                low,
                open,
                close,
                decreasing: {line: {color: 'blue'}},
                increasing: {line: {color: 'red'}},
                line: {color: 'rgba(31,119,180,1)'},
                type: 'candlestick', 
                xaxis: 'x', 
                yaxis: 'y'
            }
            console.log(info);
            console.log('>> getBitPrice done')
            return {trace, info};
        }

        function drawChart(trace, info) {
            var data = [trace];
                
            var layout = {
                dragmode: 'zoom', 
                margin: {
                    r: 10, 
                    t: 25, 
                    b: 40, 
                    l: 60
                }, 
                showlegend: false, 
                xaxis: {
                    autorange: true, 
                    domain: [0, 1], 
                    //range: ['2017-01-03 12:00', '2017-02-15 12:00'], 
                    range: [trace.x[0], trace.x[trace.x.length - 1]],
                    rangeslider: {range: [trace.x[0], trace.x[trace.x.length - 1]]}, 
                    title: info.name + ' / ' + info.market, 
                    type: 'date',
                    rangeslider: {
                        visible: false
                    }
                }, 
                yaxis: {
                    autorange: true, 
                    domain: [0, 1], 
                    range: [info.lowest_price * 0.95, info.highest_price * 1.05], 
                    type: 'linear'
                }
            };
                
            Plotly.newPlot('chartDiv', data, layout);
            console.log('>> drawChart done')
        }
    </script>
</head>
<body onload="onLoad()">
    <div id="chartDiv" style="width:100%;height:500px;"></div>
</body>
</html>

 

 

실시간으로 데이터를 가져온 뒤 그려주는 것이어서, async - await를 잘 써주자.

setTimeout 같은걸 이용하면 실시간 업데이트해서 캔들을 보여주는것도 가능할 거 같다.

Github 소스코드 참고

exam-codes-public/javascript/draw-chart.html at main · sparrow-lee/exam-codes-public (github.com)

 

 

 

 

HTML 문서에서 javascript로 http API call을 요청할 수 있다.

 

<html lang="ko">
<head>
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
    <meta charset="utf-8" />
    <title>Rest API exam</title>

    <script>
        async function onLoad() {
            console.log('onLoad')
            let jsonResponse = await testPost();
            console.log(JSON.stringify(jsonResponse));
            console.log("testPost done");
            let testDiv = document.getElementById('testDiv');
            testDiv.textContent = JSON.stringify(jsonResponse);
        }

        async function testPost() {
            const response = await fetch("https://jsonplaceholder.typicode.com/todos", {
                method: "POST",
                body: JSON.stringify({
                    userId: 2,
                    title: "My test api call",
                    completed: false,
                    name: "Hoyeong Lee"
                }),
                headers: {
                    "Content-type": "application/json; charset=UTF-8"
                }
            })
            const jsonResult = await response.json();
            return jsonResult;
        }

    </script>
</head>
<body onload="onLoad()">
    <h1>Test</h1>
    <div id="testDiv" style="width:600px;height:250px;"></div>
</body>
</html>

 

 

* 위 코드를 실행하는 페이지 보기 → Rest API exam (sparrow-lee.github.io)

 

Rest API exam

 

sparrow-lee.github.io

 

 

※ 테스트용 API 호출을 해볼 수 있게 제공하는 사이트가 있다.

JSONPlaceholder - Guide (typicode.com)

 

JSONPlaceholder - Guide

Guide Below you'll find examples using Fetch API but you can JSONPlaceholder with any other language. You can copy paste the code in your browser console to quickly test JSONPlaceholder. Getting a resource fetch('https://jsonplaceholder.typicode.com/posts/

jsonplaceholder.typicode.com

 

 

위 링크의 예시에서는 promise를 이용했고, 내가 쓴 예시에서는 비동기 함수를 사용했다.

비동기 동작을 호출하는 함수는 async로 정의하고, 비동기 동작 함수를 쓸 때는 await를 앞에 붙여주면 된다.

그러면, 코드상으로는 순차적으로 개발을 할 수 있는 편하다.

* await를 쓰지 않으면, 비동기 호출을 기다리지 않고 아래 코드들이 먼저 수행되어버릴 수 있다.

브라우저에서 F12를 누르면 console.log로 남긴 로그를 볼 수 있다.

호출결과를 받으면, testDiv 영역에 표시하도록 했다.

로컬 브라우저에서도 fetch를 이용하여, Rest API를 잘 호출할 수 있다. GET, POST 방식 모두 잘 동작하는것 같다.

Github 코드 참고 링크

exam-codes-public/javascript/js-rest-api.html at main · sparrow-lee/exam-codes-public (github.com)

 

Array와 함께 자주 사용하는 데이터 형태로 JSON이 있다.

Key, Value 쌍으로 데이터를 저장 한다고 보면 되겠다.

 

let myJson = {};    // 빈 오브젝트 생성

// 데이터를 입력하면서 생성
let myJson2 = {
  "name": "Hoyeong Lee",
  "age": 40,
  "friends": ["Peter", "Mike", "James"],
  "address": {
     "city": "Suwon",
     "country": "Korea",
  }
};

 

 

다양한 데이터 타입의 데이터를 하위에 넣을 수 있고, JSON object 자체를 또 하위에 둘 수도 있다.

물론 배열(Array)도 그렇게 가능하다.

어떻게 보면, 배열은 Key가 0부터 시작하는 숫자들로 된 JSON 과 유사하다고 보면 될 수 있겠다.

인자를 접근하는 방식은 몇가지가 있겠다.

let myInfo = {
  "name": "Hoyeong Lee",
  "age": 40,
  "friends": ["Peter", "Mike", "James"],
  "address": {
     "city": "Suwon",
     "country": "Korea",
  }
};

console.log(myInfo.name);
console.log(myInfo.address.city);
console.log(myInfo["age"]);

let key = "friends"
console.log(myInfo[key]);   // 변수에 키를 넣어서 접근

 

 

 

JSON object의 키들을 배열로 가져오기. Object.keys

JSON object의 value들을 배열로 가져오기. Object.values

 

let myInfo = {
  "name": "Hoyeong Lee",
  "age": 40,
  "friends": ["Peter", "Mike", "James"],
  "address": {
     "city": "Suwon",
     "country": "Korea",
  }
};

console.log(Object.keys(myInfo));  // ['name', 'age', 'friends', 'address']
console.log(Object.values(myInfo));  // ['Hoyeong Lee', 40, Array(3), {…}]

 

 

 

for ... in 을 이용하여 전체 인자 접근

let myInfo = {
  "name": "Hoyeong Lee",
  "age": 40,
  "friends": ["Peter", "Mike", "James"],
  "address": {
     "city": "Suwon",
     "country": "Korea",
  }
};


for (let key in myInfo) {
  console.log(`${key} = ${myInfo[key]}`);
}

 

 

 

특정 인자를 지우려면, myInfo.age = undefined; 처럼 해주어도 된다.

그리고, key 값을 동적으로 부여해야 할 필요가 있을 때 변수에 넣어서 아래처럼 값을 할당 할 수 있겠다.

let key = "gender";

let myInfo = {
  "name": "Hoyeong Lee",
  "age": 40,
  [key]: "male",
};

 

myInfo[key] = "male"; 처럼 사용 할 수도 있지만, 위처럼 한번에 Object 를 펼쳐서 assign을 해야 깔끔하게 되

는 경우가 있을 때가 있다.

JSON을 문자열로 만들기. JSON.stringify()

문자열을 JSON object로 파싱. JSON.parse()

 

let myInfo = {
  "name": "Hoyeong Lee",
  "age": 40,
  "gender": "male",
};

let str = JSON.stringify(myInfo);
console.log(str);  // {"name":"Hoyeong Lee","age":40,"gender":"male"}

let parsedInfo = JSON.parse(str);
console.log(typeof parsedInfo);  // object

 

 

JSON object를 변수로 어사인하면 참조를 하게 되므로, 실제로는 2개의 변수에서 같은 메모리상의 object를 가리키게 된다. 그래서, 한쪽을 변경하면 다른쪽도 영향을 받게 되므로, 실제 각각 별개의 JSON object 변수를 가지고 있으려면 새 JSON object를 생성한 뒤에 하위 인자들을 모두 복사해야한다.

let myInfo = {
  "name": "Hoyeong Lee",
  "age": 40,
  "gender": "male",
};

let copyInfo = myInfo;   // 두 변수는 실제 같은 객체를 가리키게 된다.
copyInfo.age = 10;
console.log(myInfo.age); // 10

copyInfo = {};
for (let key in myInfo) {
  copyInfo[key] = myInfo[key];
}

copyInfo.name = "Gil Dong";
console.log(myInfo.name); // "Hoyeong Lee"
console.log(copyInfo.name); // "Gil Dong"

 

 

다만, for ... in 으로 순환하면서 인자를 직접 복사해주었지만, 인자 자체가 또한 JSON object라면 하위 인자에서 또 같은 문제가 재현될 수 밖에 없다.

 

let myInfo = {
  "name": "Hoyeong Lee",
  "address" : {
     "country": "Korea",
     "city": "Seoul",
  }
};

let copyInfo = {};
for (let key in myInfo) {
  copyInfo[key] = myInfo[key];
}

copyInfo.address.city = "Busan";
console.log(myInfo.address.city); // "Busan"
console.log(copyInfo.address.city); // "Busan"

 

 

+ Recent posts