burning-carrot/core-javascript

What I learend about the core of the javascript. (Korean) - burning-carrot/core-javascript

github.com

(최신 버전의 글은 위 GitHub 저장소에서 확인하실 수 있습니다. 때때로 업데이트되고있어요~)

Javascript에서 this는 무엇인가?

this가 무엇인지 대답하라고 할 때 보통 상황마다 다른 this를 대답하곤 한다.

"그래서 this가 뭔데요?"

this는 현재 실행 문맥이다. 그런데 이놈은 상황마다 다르다.

실행문맥이란 말은 호출자가 누구인지와 같다.

콘텍스트(context) 객체는 this가 바라보고 있는 어떤 객체이며 역으로 표현하면 this는 context를 가리키고 있는것이다.

 

this가 변하는 상황을 먼저 나열해보자

  • global scope
  • Object's method (암시적 바인딩)
  • call(), apply(), bind() (명시적 바인딩)
  • arrow function
  • new
  • callback

이제부터 정리해보자


상황에 따른 this

global scope

전역공간에서의 this는 전역객체를 가리킨다.

  • 브라우저는 window
  • Node.js는 global

전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다.

var a = 1;

this.a; // 1

 

Object's method

함수는 자체로 독립적인 기능을 수행

메서드는 자신을 호출한 대상 객체에 관한 동작 수행 (부모 객체라고 하자)

  • 함수에서의 this : 전역객체
  • 메서드에서의 this : 호출한 객체
var func = function () {
  console.log(this);
};

(function () {
  func();
})(); // 즉시 실행함수로 실행해도, 다른 함수에서 a를 호출해도 a의 this는 Window, global이다.

var obj = {
  method: func,
};

obj.method(); // obj를 가리킨다.

 

call, apply, bind

함수에 명시적으로 this를 바인딩 할 수 있다.

call과 apply의 차이는 바인딩할 함수의 인자가 되며, 인자를 넣어주는 방법이 다르다.

func.call(this, `param1`, `param2`); // argument
func.apply(this, [`param1`, `param2`]); // array

call과 apply의 return값은 undefined이다.

또한 call, apply는 바인딩 하는 순간 실행된다.

bind의 경우는 this 바인딩만 해주고 실행은 직접 해줘야 한다.

func.bind(this, `param1`, `param2`);

 

arrow function

화살표 함수는 자신의 this가 없다.

화살표 함수는 함수를 선언할 때 this에 바인딩할 객체가 정적으로 결정되며, 화살표 함수를 둘러싸는 렉시컬 스코프(lexical scope)의 this가 사용된다.

또한 call, apply, bind를 사용해 명시적으로 this를 bind하더라도 무시한다.

var obj = {
  outer: function () {
    console.log(this); // this1

    var innerFunc = () => {
      console.log(this); // this2
    };
    innerFunc();
  },
};

obj.outer(); // this1과 this2는 동일하다.
var name = "outer";
const arrowObj = {
  name: "object",
  func: () => console.log(`${this.name} function`),
};

const funcObj = {
  name: "object",
  func: function () {
    console.log(`${this.name} function`);
  },
};

arrowObj.func(); // outer function
funcObj.func(); // object function

 

new

생성자 함수에서는 인스턴스 자신이된다. (class와 비슷함)

var Obj = function (name, age) {
  this.name = name;
  this.age = age;
};

var obj = new Obj("나비", 1); // {name:"나비", age:1}

 

callback

콜백함수의 제어권을 가지는 함수(메서드)마다 다르다

제어권을 가지는 함수가 콜백 함수에서 this를 무엇으로 할지 결정하기 때문이다.

// setTimeout의 this는 Window, Global이다.
setTimeout(function () {
  console.log(this); // Window
}, 300);

// Array.prototype.forEach에서 this는 Window, Global이다.
[1, 2, 3, 4, 5].forEach(function (x) {
  console.log(this, x); // Window
});

// eventListener에서 this는 event.target이다.
// <button id="a">클릭</button>
document.body.querySelector("#a").addEventListener("click", function (e) {
  console.log(this); // button
});

 


실전 문제들

 

7 Interview Questions on "this" keyword in JavaScript. Can You Answer Them?

7 interview questions to challenge your knowledge on "this" keyword in JavaScript.

dmitripavlutin.com

해당 문제들은 위 글에서 가져왔으며, 문제 정답 해설은 제가 직접 달았습니다. 틀린 부분은 댓글로 남겨주시면 빠르게 반영하겠습니다.

 

Variable vs property

const object = {
  message: "Hello, World!",

  getMessage() {
    const message = "Hello, Earth!";
    return this.message;
  },
};

console.log(object.getMessage()); // What is logged?

getMessage는 object의 메소드이다. 이 경우 this는 호출한 객체를 나타내므로 Hello, World!가 출력된다.

 

Cat name

function Pet(name) {
  this.name = name;

  this.getName = () => this.name;
}

const cat = new Pet("Fluffy");

console.log(cat.getName()); // What is logged?

const { getName } = cat;
console.log(getName()); // What is logged?

첫번째의 경우 object의 메소드로 호출했다. 따라서 Fluffy가 출력된다.

두번째의 경우 object의 메소드를 함수로 추출해서 실행했다.

이 때 getName은 arrow function이므로 렉시컬 스코프의 this를 기억한다.

따라서 위와 같이 this는 Pet이다. 따라서 Fluffy가 출력된다.

만약 getName이 arrow function이 아닌 일반 함수일 경우에 getName을 추출해서 사용할 경우 this는 전역객체이다.

전역객체에서 name은 선언되지 않았으므로 undefined이다. 따라서 undefined가 출력된다.

 

Delayed greeting

const object = {
  message: "Hello, World!",

  logMessage() {
    console.log(this.message); // What is logged?
  },
};

setTimeout(object.logMessage, 1000);

object의 메소드에서 this는 자기 자신이다.

그러나 우리는 setTimeout의 callback으로 object의 메소드를 인자로 넣었다.

따라서 이 함수는 메소드가 아닌 일반 함수로 동작한다.

이 일반 함수는 arrow function이 아니기 때문에 렉시컬 스코프를 기억하지 못하고 따라서 this는 전역객체가 된다.

따라서 undefined가 출력된다.

만약 arrow function인 경우에는 어떻게될까?

이는 객체에서 프로퍼티에 arrow function 함수를 할당하는 형태가 된다.

var message = "new world";

const object = {
  message: "Hello, World!",

  logMessage() {
    console.log(this.message); // What is logged?
  },

  logMessageArrow: () => {
    console.log(this.message); // What is logged?
  },
};

object.logMessage(); // 'Hello, World!'
object.logMessageArrow(); // 'new world'

setTimeout(object.logMessage, 1000); // 'new world'
setTimeout(object.logMessageArrow, 1000); // 'new world'

이 경우 arrow function는 자신의 this를 가지고 있지 않다. 따라서 이 this는 전역 객체가 된다.

따라서 위 경우는 다음과 같이 출력된다. 객체의 프로퍼티로 arrow function을 할당하는 경우에는 무조건 전역 객체가 this이기 때문이다. 이는 callback으로 사용될 때도 어차피 전역이므로 동일하다.

함수 단위 스코프를 이용하는 경우는 다음과 같다.

var foo = "foo";

function hello() {
  const object = {
    message: "Hello, World!",
    foo: "bar",

    logMessage() {
      console.log("method", this.foo, this.message);
    },

    logMessageArrow: () => {
      console.log("arrow", this.foo, this.message);
    },
  };

  object.logMessage(); // 'method' 'bar' 'Hello, World!'
  object.logMessageArrow(); // 'arrow' 'foo' undefined

  setTimeout(object.logMessage, 1000); // 'method' 'foo' undefined
  setTimeout(object.logMessageArrow, 1000); // 'arrow' 'foo' undefined
}

hello();

class와 new 키워드를 사용하는 경우는 callback으로 메소드를 사용할 때 다음과 같다.

여기서 arrow function의 경우 렉시컬 스코프를 기억하기 때문이다.

class Object {
  constructor(message) {
    this.message = message;
    this.logMessage = function () {
      console.log("method", this.message);
    };
    this.logMessageArrow = () => {
      console.log("arrow", this.message);
    };
  }
}

const object = new Object("Hello, World!");

setTimeout(object.logMessage, 1000); // 'method' undefined
setTimeout(object.logMessageArrow, 1000); // 'arrow' 'Hello, World!'

 

Artificial method

const object = {
  message: "Hello, World!",
};

function logMessage() {
  console.log(this.message); // "Hello, World!"
}

// Write your code here...

this를 강제로 바인딩 하는 문제이다.

bind, apply, call 등을 이용해 강제로 바인딩을 할 수 있다.

혹은 object의 메소드로 지정한 후 호출해 사용할 수 있다.

object.logMessage = logMessage;
object.logMessage();

// Using func.call() method
logMessage.call(object);

// Using func.apply() method
logMessage.apply(object);

// Creating a bound function
const boundLogMessage = logMessage.bind(object);
boundLogMessage();

 

Greeting and farewell

const object = {
  who: "World",

  greet() {
    return `Hello, ${this.who}!`;
  },

  farewell: () => {
    return `Goodbye, ${this.who}!`;
  },
};

console.log(object.greet()); // What is logged?
console.log(object.farewell()); // What is logged?

이 경우에 arrow function의 this는 전역객체이다.

따라서 greet의 경우에는 Hello, World!가, farewell의 경우에는 Goodbye, undefined!가 출력된다.

 

Tricky length

var length = 4;
function callback() {
  console.log(this.length); // What is logged?
}

const object = {
  length: 5,
  method(callback) {
    callback();
  },
};

object.method(callback, 1, 2);

object.method에 인자로 넣은 callback을 실행했다.

callback이 인자로 실행되므로 여기서 this는 전역객체이다. 따라서 4가 출력된다.

 

Calling arguments

var length = 4;
function callback() {
  console.log(this.length); // What is logged?
}

const object = {
  length: 5,
  method() {
    arguments[0]();
  },
};

object.method(callback, 1, 2);

이 경우 method에서 argument는 특수한 변수이며 다음과 같다.

{
  0: callback,
  1: 1,
  2: 2,
  length: 3
}

여기서 arguments[0]의 callback을 호출할 경우 이 callback은 arguments의 메소드처럼 동작한다.

따라서 이 경우 this는 arguments를 나타내므로 arguments의 길이인 3이 출력된다.

(object.method에 인자로 값을 3개 넣었으므로)

이벤트 위임을 이용해 블록 옮기기 구현하기

투두리스트

https://move-block-event-delegation.herokuapp.com/index.html 로 가시면 예제 데모를 확인하실 수 있습니다.

 

이 글에 나온 예제소스는 https://github.com/changicho/move_block-event_delegation 에서도 확인할 수 있습니다.

 


블록의 이동을 구현할 때, 각 블록마다 이벤트를 등록하진 않으셨나요?

 

각각 이벤트를 등록하고 잘 동작하게 구현하셨다면 다음 글을 읽어보시는것은 어떨까요?

왜 이벤트 위임(delegation)을 해야 하는가?

성능 측정 결과는 매우 실망스러웠다.
각주의 추가 삭제를 반복할수록 메모리 누수로 인해 메모리 사용량이 누적되는 것을 발견했다.
무슨 이유로 메모리 사용량이 누적되는 것일까?

 

각 블록들이 추가와 삭제가 빈번하게 일어난다면, 이벤트를 등록하고 삭제해줘야 합니다.

만약 등록된 이벤트를 삭제하지 않는다면 메모리 누수가 발생할 수 있어요.

 

그렇다면 어떻게 하나의 이벤트에서 하위 이벤트들을 관리할 수 있을까요?

저는 이벤트 위임을 이용해 이를 구현했습니다.

 

그렇다면 이벤트 위임이란 무엇일까요??

이벤트 위임

javascript.info event-delegation

이벤트 위임을 사용하면 요소마다 핸들러를 할당하지 않고, 요소의 공통 조상에 이벤트 핸들러를 단 하나만 할당해도 여러 요소를 한꺼번에 다룰 수 있습니다.

쉽게 말하면 각각 개별 이벤트를 등록해야할 자식들 대신, 부모에 이벤트를 등록하고 분기처리 하는 것입니다.

 

다음과 같은 구조의 html이 존재한다고 가정해봅시다.

<div class="parent">
  <div class="child1"></div>
  <div class="child2"></div>
  <div class="child3"></div>
</div>

 

그리고 각각 다음과 같은 이벤트를 등록한다고 해봅시다.

// child1, 2, 3는 querySelect 등으로 찾아왔다고 가정합시다.

child1.addEventListener("click", () => {
  console.log("child1");
});
child2.addEventListener("click", () => {
  console.log("child2");
});
child3.addEventListener("click", () => {
  console.log("child3");
});

 

위와 같은 코드는 잘 동작합니다. 하지만 child가 매우 많아진다면 어떻할까요??

<div class="parent">
  <div class="child1"></div>
  <!-- ~ -->
  <div class="child1000000"></div>
</div>

 

이 경우에는 querySelectorAll로 찾아서 하나하나 등록하는 방법도 있겠지만, 매우 번거롭습니다.

 

이 경우에 다음과 같은 것은 어떨까요?

parent.addEventListener('click',(event)=>{
  const class = event.target.className;

  switch(class){
    case 'child1':{
      console.log('chlid1');
    }
    // ...
  }
})

하나의 이벤트로 여러개의 이벤트를 대신할 수 있습니다.

 

더 최적화 하고 싶다면 다음 코드는 어떨까요??

parent.addEventListener('click',(event)=>{
  const class = event.target.className;

  console.log(class); // 단순히 자기 자신의 class를 표시할 수 있습니다.
})

각 child div마다 dataset등을 설정해서, 하나의 로직으로 처리하게 할 수도 있습니다.

 

블록 이동 만들어보기

사용할 예제 소스

그렇다면 이제 블록 이동을 만들어 볼까요??

우선 다음과 같은 구조의 html을 만들어주세요

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>블록 옮기기 테스트</title>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <ul class="first">
      <li class="start"></li>
      <li>
        <p>하나</p>
      </li>
      <li>
        <p>둘</p>
      </li>
    </ul>
    <ul class="second">
      <li class="start"></li>
      <li>
        <p>셋</p>
      </li>
    </ul>
    <ul class="third">
      <li class="start"></li>
      <li>
        <p>넷</p>
      </li>
    </ul>
    <div class="hover"></div>
  </body>
  <script src="./main.js"></script>
</html>
body {
  display: flex;
  flex-direction: row;
}

ul {
  width: 300px;
  height: 600px;

  margin: 10px;
  padding: 10px;

  background-color: #dddddd;
  border-radius: 10px;
}

li {
  list-style: none;
  padding: 10px;
  margin: 10px 0 10px 0;

  width: 280px;

  background-color: #888888;
  border-radius: 10px;

  text-align: center;
  color: #ffffff;
  user-select: none;
}

li.start {
  height: 0;
  border-radius: 0;
  padding: 0;
  margin: 0;
}

.hover {
  position: absolute;
  padding: 0;
  margin: 0;

  transform: rotate(-5deg);
}

.temp {
  opacity: 0.5;
}

각 ul은 블록을 쌓을 column을, li는 블록을 의미합니다.

 

이 때 편의상 ul의 맨 처음에는 시작 ul (start point)을 넣음에 유의해주세요.

클릭 구현하기

다음과 같은 기본 javascript 코드를 생성합니다.

 

타겟으로 설정할 element를 찾아옵니다.

const body = document.querySelector("body");
const hover = document.querySelector(".hover");

전역 변수로 사용할 변수들을 설정해주세요

let clicked = false;
let hoverLi = undefined;

다음으로 이벤트 핸들러들을 각각 작성해줍니다

function mousedown(event) {
  // 마우스 왼쪽클릭 & 터치가 아닌 경우 예외처리합니다.
  if (event.button !== 0) {
    return;
  }
  // 클릭했으므로 clicked를 설정합니다.
  clicked = true;
  // 현재 클릭한 element에서 가장 가까운 li 태그를 찾습니다.
  targetRemove = event.target.closest("li");
  if (targetRemove === null || targetRemove.className === "start") {
    // 만약에 li태그를 찾지 못했거나, 시작점을 클릭했다면 return합니다.
    return;
  }

  // 현재 삭제하려고 하는 target은 li태그입니다.
  targetLi = targetRemove;
  // 내부 값을 복사한 element를 마우스를 따라다닐 hover로 설정합니다.
  hoverLi = targetRemove.cloneNode(true);
  // target을 불투명하게 하기 위해 class를 넣어주세요
  targetLi.classList.add("temp");

  const { pageX, pageY } = event;

  // hover에 아까 clone한 element를 붙여넣어줍니다.
  hover.appendChild(hoverLi);

  // 마우스 중앙에 hover가 오도록 설정합니다.
  hover.style.left = pageX - hover.offsetWidth / 2 + "px";
  hover.style.top = pageY - hover.offsetHeight / 2 + "px";
}
function mouseup() {
  // 클릭되지 않은 상태면 실행하지 않습니다.
  if (!clicked) {
    return;
  }

  clicked = false; // 클릭이 종료되었으므로 했으므로 clicked를 설정합니다.
  if (targetLi) {
    // targetLi가 있으면 class를 제거해주세요
    targetLi.classList.remove("temp");
  }
  if (hoverLi) {
    // hoverLi는 hover가 끝나므로 더이상 필요하지 않습니다.
    hoverLi.remove();
  }
  // 아래 전역변수들을 초기화해주세요
  hoverLi = undefined;
  targetLi = undefined;
}

이벤트 리스너를 등록해줍니다.

body.addEventListener("mousedown", mousedown);
body.addEventListener("mouseup", mouseup);

 

자세한 설명은 주석으로 대신하겠습니다.

 

위 코드를 적용하면 다음과 같은 결과를 얻을 수 있습니다.

 

클릭하면 hover가 발생합니다

 

해당 li를 클릭할 때, hover가 구현되는것을 알 수 있습니다.

 

hover로 이동된 target 말고 기존 자리에 임시 li도 남아있는것을 확인할 수 있습니다.

이동 구현하기

이제 제일 중요한 이동을 구현해볼 건데요,

 

이동이 다음 순서로 일어남에 유의해주세요

  1. 마우스 클릭
    1. li태그를 찾음
    2. hover에 표시
  2. 마우스 움직이기 (클릭한 상태로)
    1. hover의 좌표를 마우스 좌표에 맞춰서 변경
    2. 현재 마우스 좌표가 어떤 li 위에 있는지에 따라서 분기처리
      • 현재 li 앞에 이동? 뒤에 이동?
  3. 마우스에서 손 떼기
    1. hover 초기화하기
    2. 전역으로 선언한 부분 초기화하기

그런데 hover가 마우스를 따라다니는 경우에, 마우스 좌표에 존재하는 li를 어떻게 찾을 수 있을까요?

hover가 마우스 좌표를 항상 따라다녀서 hover만을 찾아올 텐데요.

 

이를 위해서 hover를 잠시 가려두는 방법을 사용합니다.

// 잠시 현재 hover element를 가리고 현재 좌표의 element를 가져온다
hover.hidden = true;
// do something
hover.hidden = false;

이렇게 가져오는 element에서 주요하게 고려해야 할 종류는 2가지가 있습니다.

  • li
  • ul

이 때 ul의 경우에는 간단히 맨 위에 붙일 경우와, 맨 아래에 붙일 경우만 나누겠습니다.

 

ul인 경우에 시작 li 보다 좌표가 위에 있다면, 첫번째에 추가하면 되고, 그 외의 경우는 맨 아래에 추가하면 될 것입니다.

 

li의 경우는 좀 복잡한데요, 우선 다음 함수를 작성해주세요.

// element2이 element1보다 앞에 있는지 검사하는 함수입니다.
function isBefore(element1, element2) {
  if (element2.parentNode === element1.parentNode) {
    for (let cur = element1.previousSibling; cur; cur = cur.previousSibling) {
      if (cur === element2) {
        return true;
      }
    }
  }

  return false;
}

위 함수를 이용해서 저희가 추가하려는 element를 뒤에 붙일지 앞에 붙일 지 정할 수 있습니다.

 

따라서 mousemove에 할당할 callback은 다음과 같이 구성됩니다.

function mousemove(event) {
  if (!clicked || !hoverLi) return;

  // pageX, pageY 는 모든 페이지 기반
  // clientX, clientY 는 현제 보이는 화면 기반
  const { pageX, pageY } = event;

  // 잠시 현재 hover element를 가리고 현재 좌표의 element를 가져온다
  hover.hidden = true;
  const elemBelow = document.elementFromPoint(pageX, pageY);
  const li = elemBelow.closest("li");
  const ul = elemBelow.closest("ul");
  hover.hidden = false;

  // 이동할 때마다 hover의 위치를 수정해줍니다.
  hover.style.left = pageX - hover.offsetWidth / 2 + "px";
  hover.style.top = pageY - hover.offsetHeight / 2 + "px";

  // 현재 마우스가 ul을 가리키는 경우에는 li를 찾을 수 없습니다.
  if (!li) {
    // 만약 ul을 가리키고 있는 경우가 맞다면??
    if (ul) {
      const start = ul.querySelector(".start");
      const { top } = start.getBoundingClientRect();

      // 시작점보다 위에있는 경우에 맨 앞에 붙이고
      if (top > pageY) {
        start.parentNode.insertBefore(targetLi, start.nextSibling);
      } else {
        // 그 외에는 맨 아래에 붙이면 되겠네요.
        ul.appendChild(targetLi);
      }
    }
    return;
  }

  // 만약 같은 ul에서 li가 현재 좌표에 있는 target보다 앞에있으면
  // target을 li 앞으로 옮겨줍니다.
  if (isBefore(targetLi, li) && li.className !== "start") {
    li.parentNode.insertBefore(targetLi, li);
  } else if (li.parentNode) {
    // 그 외에는 뒤로 이동시켜 버리면 됩니다.
    li.parentNode.insertBefore(targetLi, li.nextSibling);
  }
}

// 마지막으로 이벤트를 등록해주세요
body.addEventListener("mousemove", mousemove);

 

MDN insertBefore

이 때 insertBefore를 이용해서 이전에 위치했던 targetLi를 이동시켜 버리는 것에 유의해주세요.

만약 주어진 자식 노드가 document에 존재하는 노드를 참조한다면, insertBefore() 가 자식 노드를 현재 위치에서 새로운 위치로 옮깁니다. (노드를 다른 노드에 추가하기 전에 상위 노드에서 제거할 필요가 없습니다)

마우스가 화면을 벗어날 때

만약 마우스가 화면을 벗어나는 경우에는 어떻게 할까요?

 

가장 간단한 방법으로는, 마우스 클릭이 끝났을 때의 동작을 시키는 것입니다.

다음과 같은 방법을 사용합시다.

function mouseleave() {
  // 화면을 벗어났을 때 click한 상태가 아니라면 return합니다.
  if (!clicked) {
    return;
  }
  // mouseup 함수를
  mouseup();
}

body.addEventListener("mouseleave", mouseleave);

자 이렇게 블록이동의 구현을 완료했습니다.

 

어떤가요? 마우스 이벤트 만으로 그럴듯한 블록이동의 구현이 완료되었습니다.

 

drag & drop api를 사용할 경우 몇몇 코드를 삭제할 수도 있습니다.

한번 위 코드를 drag & drop api 에 맞춰서 수정해보세요.

Javascript없이 HTML 과 CSS 로는 단순히 정적인 페이지를 보여주는것만 가능하다.

(CSS 옵션 및 애니메이션이 아닌 데이터의 요청 등의 '변화' 를 기준으로 정적임을 판단.)

 

만약 새로고침 없이 동적으로 데이터를 받아오고 싶다면 Javascript로 서버에 통신을 요청하면 된다.

 

이런 방법을 어디에 사용할 수 있을까??

사실 현대 웹 에서는 UX의 모든곳에 사용하고 있다.

 

Chrome 브라우저의 개발자도구 > 네트워크 탭으로 확인해보자.

 

네이버 모바일 페이지의 홈 화면이다.
뉴스 탭을 클릭할때 데이터들이 받아지는것을 볼 수 있다.

 

이 데이터들의 통신을 AJAX 통신이라고 한다. (Asynchronous JavaScript And XML)

Asynchronous (비동기) 임에 주의하자.

 

Ajax 통신을 간단히 말하면 "서버와 통신하기 위해 XMLHttpRequest 객체를 사용하는 것" 이다.

 

Javascript는 브라우저에서 Ajax 통신을 지원하기 위해 자연스럽게 비동기를 지원하게 되었고,

브라우저의 자바스크립트 엔진은 싱글 스레드 기반이므로, 하나의 스레드로 이를 처리해야만 했다.

 

이 글에서는 Ajax 통신과 비동기에 대해서 설명한다.

 

 

Ajax 시작하기

본 문서는 AJAX의 기본을 익힐수 있도록 해주며, 두 가지 간단한 훈련용 예제를 제공합니다.

developer.mozilla.org


Ajax (Asynchronous JavaScript And XML)

소개글에서 웹 페이지에서 Ajax 통신을 어떻게 사용하는 지를 설명했다.

 

만약, 첫 메인페이지에서 다른 탭들의 데이터를 한꺼번에 로딩한다면 비동기 통신을 할 필요가 없다.

 

다만 이 경우 초기 브라우저 로딩 속도가 매우 느려지며, 사용자가 사용하지 않을 가능성이 있는 정보까지 요청한다.

거기다 사용자가 많아질 경우 서버에 큰 부담이 된다.

 

Ajax 통신으로 JSON, XML, HTML 그리고 일반 텍스트 형식 등을 포함한 다양한 포맷을 주고받을 수 있다.

그 중에서 일반적으로 사용이 편한 JSON 형식을 주고받는 방법을 사용한다.

JSON 

 

JSON (JavaScript Object Notation)은 경량의 DATA-교환 형식이며, 

Javascript에서 객체를 만들 때 사용하는 표현식 이다.

 

이 형식은 사람이 읽고 쓰기에 용이하며, 기계가 분석하고 생성함에도 용이하다.

 

특정 언어에 종속되지 않는다는 특징이 있다.

따라서 대부분의 프로그래밍 언어에서 JSON 포맷의 데이터를 핸들링 할 수 있는 라이브러리를 제공한다.

 

우선 JSON 데이터를 확인해보자

 

{
  "squadName": "Super hero squad",
  "homeTown": "Metro City",
  "formed": 2016,
  "secretBase": "Super tower",
  "active": true,
  "members": [
    {
      "name": "Molecule Man",
      "age": 29,
      "secretIdentity": "Dan Jukes",
      "powers": [
        "Radiation resistance",
        "Turning tiny",
        "Radiation blast"
      ]
    },
    {
      "name": "Madame Uppercut",
      "age": 39,
      "secretIdentity": "Jane Wilson",
      "powers": [
        "Million tonne punch",
        "Damage resistance",
        "Superhuman reflexes"
      ]
    }
  ]
}

JSON 데이터는 Key 와 Value로 이루어진 쌍의 데이터들을 가진다. : { String key : String value }

Key는 유일해야하며, Value로는 일반 값과 배열, JSON을 포함한 값들을 가진다.

 

JSON 내부에 JSON데이터를 넣을 수 있으므로 위 예시와 같이 응용할 수 있다.

 

JSON을 다룰 때 자주 사용하는 메소드로는 다음 두가지가 있다.

  • JSON.parse(JSON으로 변환할 문자열) : JSON 형식의 텍스트를 자바스크립트 객체로 변환한다.
  • JSON.stringify(JSON 문자열로 변환할 값) : 자바스크립트 객체를 JSON 텍스트로 변환한다.

다음 예제를 실행해보자

var jsonText = '{ "key": "value" }';
var realObject = JSON.parse(jsonText);

realObject	// {key: "value"}

var realObjectTojsonText = JSON.stringify(realObject);

realObjectTojsonText // "{"key":"value"}"

형식이 맞을경우 문자열을 JSON 데이터로 변환할 수 있다.

 

Ajax를 실행해보자

function ajax() {
 var oReq = new XMLHttpRequest();
 
 oReq.addEventListener("load", function() {
   console.log(this.responseText);
 });    
 
 // 요청방식, url, 비동기적으로 실행될지 boolean (생략가능)
 oReq.open("GET", "https://hacker-news.firebaseio.com/v0/topstories.json");
 oReq.send();
}

// 위 함수를 실행시켜보자
ajax()

출처 : 부스트코스 강의 내용 중

 

oReq는 XMXMLHttpRequest 로 생성된 객체이다.

 

이 객체에 이벤트 리스너로 load 되었을 때 console로 responseText를 출력하도록 설정했다.

여기서 this 바인딩을 사용했는데, 이 this는 함수의 인자로 자동으로 전달되는 객체를 가리킨다.

 

this 바인딩이 어려울 경우 다음 코드로 이해해도 된다.

function reqListener (e) {
    console.log(e.responseText);
}

 oReq.addEventListener("load", reqListener);  

 

open 메소드를 통해 요청방식, 주소, 비동기 여부를 지정하고,

send 메소드를 이용해 보낸다.

 

 

ajax함수를 실행시킨 경우 무언가 데이터가 출력되는것을 확인할 수 있다.

 

비동기

여기서 데이터의 출력이 '조금 늦게' 일어나는것을 유심히 살펴보자.

Chrome 개발자도구의 네트워크 탭을 열고 확인해보자

 

ajax()를 실행시키고 나서 데이터를 요청 후 받아오는데 171ms가 걸렸다.

요청처리가 완료되면(서버에서 응답이 오면) load이벤트가 발생하고, 콜백함수가 실행된다.

이 콜백 함수는 console.log를 실행하는 함수이다.


콜백함수가 실행될 때, ajax함수는 이미 콜스택에서 사라진 상태이다.

즉 ajax함수에서 oReq.send 가 끝날때까지 기다리지 않고, 서버에 요청만 보낸 뒤 ajax함수는 끝난것이다.

이는 setTimeout함수의 콜백함수의 실행과 유사하게 동작하는 '비동기'로직 이다.

 

자바스크립트는 싱글스레드이므로, 중간 중간 서버에 데이터를 요청하거나 setTimeout등과 같은 비동기 처리를 구현하기 위해 다음과 같은 구조를 사용한다.

 

자바스크립트 엔진, 이벤트 큐, WEb API

다음 동영상을 보면 더 자세한 설명이 나와있다.

 

 

Ajax의 응답 처리

서버로부터 받아온 JSON 데이터는 문자열 형태이다.

이는 문자열이므로 Javascript에서 사용하기 위해 문자열을 JSON으로 변화시켜주어야한다.

 

이때 JSON.parse 메소드를 사용한다.

var json객체로변환된값 = JSON.parse("서버에서 받은 JSON 문자열");

Ajax의 단점

동적인 웹과, 필요한 데이터만 전송할 수 있는 방식인 Ajax에는 다음과 같은 단점이 존재한다.

  • 히스토리 관리가 되지 않는다. (브라우저의 뒤로가기 앞으로가기 등과 상관 없는 데이터 전송) 
  • 연속으로 데이터를 요청하면 서버 부하가 증가할 수 있다.
  • XMLHttpRequest를 통해 통신을 하는 경우사용자에게 아무런 진행 정보가 주어지지 않는다.
  • 그래서 아직 요청이 완료되지 않았는데 사용자가 페이지를 떠나거나 오작동할 우려가 발생하게 된다. 

진행정보가 주어지지 않아 사용자가 로딩 중에 이탈하는 경우가 발생할 수 있음에 유의하자!

 

히스토리는 History객체를 의미한다. 자세한 내용은 아래 링크에서 확인하자.

 

History

History 인터페이스는 브라우저의 세션 기록, 즉 현재 페이지를 불러온 탭 또는 프레임의 방문 기록을 조작할 수 있는 방법을 제공합니다.

developer.mozilla.org

 


AJAX의 경우 비동기 방식이므로, Javascript의 매우 중요한 성질 중 하나를 사용하고 있다.

 

처리 과정을 파악하고 있다면, 앞으로 마주할 비동기 문제에 좀더 유연하게 대처할 수 있을것이다.

 

 

[LECTURE] 4) Ajax통신의 이해 : edwith

들어가기 전에 브라우저의 새로고침 없이 데이터를 얻어오는 방법이 있습니다. 이는 사용자가 더 빠르게 변경된 데이터를 화면의 새로고침 없이 확인할 수 있는 방법으로 더 좋은 UX(U... - 부스트코스

www.edwith.org

 

 

[LECTURE] 1) Ajax 응답 처리와 비동기 : edwith

들어가기 전에 브라우저의 새로고침 없이 데이터를 얻어오는 방법이 있습니다. 더 좋은 UX(User Experience)를 제공하는 좋은 방법이니, 알아보도록 하죠.     학습 목... - 부스트코스

www.edwith.org

 

우리가 무언가를 클릭하거나, 현재 보고있는 창을 새로고침할 때, 창을 닫을 때 등등

브라우저에서 일어나는 동작에 대해서 Event가 발생한다.

 

즉 브라우저는 Event 기반으로 동작한다고 할 수 있다.

 

이러한 다양한 종류의 Event에 대해서 알아보자

 

https://www.edwith.org/boostcourse-web/lecture/16700/

 

[LECTURE] 3) Browser Event, Event object, Event handler : edwith

들어가기 전에 어떤 영역을 마우스 클릭하거나, 화면을 스크롤 하거나 하는 작업에 따라서 브라우저는 반응합니다. 이런 것들은 모두 브라우저가 Event기반으로 동작되게 만들어졌기 때... - 부스트코스

www.edwith.org


Event객체

브라우저에서 발생하는 모든 동작에 대해서 이벤트가 발생한다.

  • 클릭
  • 브라우저의 화면의 크기를 마우스로 조정
  • 마우스 휠로 스크롤
  • 마우스로 어떤 것을 이동

모든 DOM 노드는 이런 신호를 만들어 낸다. 하지만 이벤트는 DOM에만 한정되진 않는다.

 

https://ko.javascript.info/introduction-browser-events

 

브라우저 이벤트 소개

 

ko.javascript.info

 

자주 사용되는 이벤트는 다음과 같다.

 

마우스 이벤트

  • click : 요소 위에서 마우스 왼쪽 버튼을 눌렀을 때
  • contextmenu : 요소 위에서 마우스 오른쪽 버튼을 눌렀을 때
  • mouseover, mouseout : 마우스 커서를 요소 위로 움직였을 때, 커서가 요소 밖으로 움직였을 때
  • mousedown, mouseup : 요소 위에서 마우스 왼쪽 버튼을 누르고 있을 때, 마우스 버튼을 뗄 때
  • mousemove : 마우스를 움직일 때

폼 요소 이벤트:

  • submit : 사용자가 <form>을 제출할 때
  • focus : 사용자가 <input>과 같은 요소에 포커스 할 때

키보드 이벤트

  • keydown과 keyup : 사용자가 키보드 버튼을 누르거나 뗄 때

문서 이벤트

  • DOMContentLoaded : HTML이 전부 로드 및 처리되어 DOM 생성이 완료되었을 때

CSS 이벤트

  • transitionend : CSS 애니메이션이 종료되었을 때

이 중에서 마우스와 키보드 이벤트를 살펴보면

"버튼을 누를 때" 와 "버튼을 뗄 때" 두 가지 이벤트가 존재한다.

 

이는 단순히 이벤트를 binding할 때 간과할 수 있는 부분인데, 예를 들어 키 입력에 대해 이벤트를 바인딩 한다고 가정해보자.

 

keydown으로만 이벤트를 바인딩 했을 때 다음과 같은 경우를 생각해 볼 수 있다.

  1. 'A'키를 누른 상태에서 때지 않고
  2. 'B'키를 입력하는 경우

즉 이벤트의 순서가 꼭 down > up > down > up 의 순서가 아닐수도 있는 것이다.

Event listener

브라우저는 Event를 발생시켜준다. 개발자는 그때 할 동작을 등록할 수 있다.

다시 말해, HTML엘리먼트별로 어떤 이벤트(주로 키보드나 마우스 관련)가 발생했을 때 특정 행위를(어떤 일) 하고 싶다면, 대상엘리먼트를 찾고 어떤 일을 등록하면 된다.

 

간단한 코드부터 살펴보자

var target = document.querySelector(".target");

target.addEventListener("click", function(e){
  console.log(e);
}, false);

https://developer.mozilla.org/ko/docs/Web/API/EventTarget/addEventListener

 

EventTarget.addEventListener()

EventTarget의 addEventListener() 메서드는 지정한 이벤트가 대상에 전달될 때마다 호출할 함수를 설정합니다.

developer.mozilla.org

위 코드에서 false 부분은 option이다. option의 의미는 MDN 문서를 통해 알아볼 수 있다.

 

예제에 사용한 false 는 capture속성이다.

 

capture : DOM 트리의 하단에 있는 EventTarget 으로 전송하기 전에, 등록된 listener 로 이 타입의 이벤트의 전송여부를 나타내는 Boolean 입니다.

 

그렇다면 다음 코드에 인자로들어가는 함수 function의 인자 e는 무엇일까?

 

이벤트 객체

 

위 코드를 실행시키기 위해 다음과 같은 html 파일을 제작하자

 

<html>
<body>
  <div id="target">aa</div>
</body>

<script>
  var el = document.getElementById("target");
  
  el.addEventListener("click", function(event){
   console.log(event);
  }, false);
</script>
</html>

콘솔에는 다음과 같이 출려된다.

MouseEvent {isTrusted: true, screenX: 1008, screenY: 153, clientX: 16, clientY: 19, …}

 

이 중에서 가장 많이 사용하는것은 Event 객체의 target, currentTarget property이다.

Event.target은 이벤트가 발생한 element를 나타낸다.

 

그렇다면 currentTarget은 무엇일까?

Event.currentTarget의 경우 이벤트가 바인딩된 요소, 해당하는 요소를 반환한다.

 

아래의 예제를 살펴보자

<div onclick="checkTarget();">
  <span>test</span>
</div>

<script>
function checkTarget(event) {
  var el = event.currentTarget;
  console.log(el);
}
</script>

만약 사용자가 div 내부의 span 태그를 클릭한 경우 각각은 다음과 같다.

 

Event.target : 클릭된 span 태그

Event.currentTarget : 이벤트가 바인딩된 div 요소를 반환

 

만약 이벤트를 binding한 곳과 클릭한 곳의 depth가 깊어지는 경우 Event.target 만으로는 어떤 요소가 클릭되어 반환되어야하는지 알기 쉽지 않다.

이 경우 currentTarget을 이용해 이벤트가 바인딩 된 곳을 알 수 있다.

이벤트 버블링 (전파)

다음과 같은 구조를 생각해보자

 

Wijmo HTML Events Capturing and Bubbling

Element 1, Element 2, Element 3모두에 이벤트 리스너를 등록한 상태에서, Element 3 를 클릭한 경우에는 어떻게 될까?

Element 3는 Element 2에 속하고, Element 2는 Element 1 에 속하기 때문에 3개의 이벤트가 발생한다.

 

이것을 이벤트 전파라고 한다.

 

이벤트 전파는 버블링과 캡처링 방식 두 가지 방식으로 동작한다.

클릭한 지점이 하위엘리먼트 이더라도, 그것을 감싸고 있는 상위 엘리먼트까지 올라가면서 이벤트리스너가 있는지 찾는 과정이다. 

 

다음의 경우를 생각해 보자

<ul>
  <li>
    <div>
    	<!-- something 1 -->
    </div>
  </li>
  <li>
    <div>
    	<!-- something 2 -->
    </div>
  </li>
  <li>
    <div>
    	<!-- something 3 -->
    </div>
  </li>
</ul>

위와 같은 구조에서 각 li 태그마다 이벤트 리스너를 직접 할당해 줄 수 있다.

그러나 이 경우 브라우저는 li 태그의 갯수만큼 이벤트 리스너를 기억하고 있어야 한다.

 

이 경우 다음과 같은 방법으로 최적화 할 수 있다.

  1. 최 상위 태그에 이벤트리스너를 등록하고
  2. Event.target을 이용해 분기처리 (각각의 li태그를 의미하기 때문에)

단순히 정적인 웹을 표기하고자 한다면 이벤트는 필요 없다. 그러나 동적인 웹을 제작하기 위해선 이벤트는 필수적이다.

 

그러나 브라우저가 너무 많은 이벤트핸들러를 기억하고 있는 것은 자원의 낭비일수도 있다.

 

따라서 브라우저가 너무 많은 이벤트핸들러를 기억하지 않게 하고, DOM이 추가되거나 삭제 될때 Event등록을 매번 해주지 않도록 최적화 하는 과정이 필요하다.

 

 

자바스크립트의 변수, 함수에는 스코프가 존재한다.

스코프는 자바스크립트에서 어떤 변수들에 접근할 수 있는지를 나타낸다.

 

ES5 까지는 함수 레벨의 스코프(function scope)만 사용 가능했으나,

ES6 부터는 let과 const로 블록 레벨의 스코프(block scope)를 사용할 수 있다.

 

이 스코프에 대해서 알아보자

 

https://www.edwith.org/boostcourse-web/lecture/16693/

 

[LECTURE] 1) 자바스크립트 변수-연산자-타입 : edwith

들어가기 전에 컴파일 단계가 없는 자바스크립트의 type(형)은 실행단계에서 타입이 결정됩니다. 변수선언은 어떻게 정의하고, 자바스크립트의 타입은 어떤 것들이 있는지 확인해봅니다.... - 부스트코스

www.edwith.org


스코프와 레벨

스코프는 다음과 같이 구성된다

  • 전역 스코프
  • 지역 스코프
    • 함수 레벨 스코프
    • 블록 레벨 스코프

앞서 블록 레벨 스코프, 함수 레벨 스코프를 설명했는데 전역 스코프와 지역 스코프는 무엇인가?

 

아래 내용을 보며 살펴보도록 하자

전역 스코프

변수가 함수 바깥이나 {} 바깥 에서 선언되었다면, 전역 스코프에 정의된다.

let a = 'aa'

위의 a는 어디에서나 접근 가능하다.

 

변수를 글로벌 변수로 선언했을 경우, 이는 전역 스코프에 정의된다고 알 수 있다.

 

let a = 'first'
let a = 'second' // 이미 선언되어있음

전역 스코프에서 중복 재할당의 문제가 발생할 수 있으므로 주의해야 한다.

 

지역 스코프

지역 스코프는 특정 영역에서만 사용할 수 있는 변수이다.

지역 스코프는 다음과 같이 2가지로 이루어진다

  • 함수 레벨
  • 블록 레벨

함수 레벨 스코프

function a(){
  var b = 2;
}

위와 같이 선언했을 때, b는 함수 a에서만 사용할 수 있다.

함수 레벨 스코프는, 함수 내에서만 사용 가능한 변수를 의미한다.

 

함수 내에서 선언된 변수는 함수 내에서만 유효하며 함수 외부에서는 참조할 수 없다.

즉, 함수 내부에서 선언한 변수는 지역 변수이며 함수 외부에서 선언한 변수는 모두 전역 변수이다.

 

블록 레벨 스코프

블록 내부에서 const, let으로 변수를 선언한다면, 이 변수들은 블록 내부에서만 사용이 가능하다.

{
  const a = 'hello world'
  console.log(a) // 'hello world'
}
console.log(a) // Error, a is not defined

모든 코드 블록(함수, if 문, for 문, while 문, try/catch 문 등) 내에서 선언된 변수는 코드 블록 내에서만 유효하다.

따라서 코드 블록 외부에서는 참조할 수 없다.

 

즉, 코드 블록 내부에서 선언한 변수는 지역 변수이다.


정리하자면 javascript는 ES5까지는 함수 레벨 스코프만 제공했으나,

ES6부터는 let, const를 이용해 블록 레벨 스코프 선언이 가능하다.

 

const를 먼저 사용하자. 재할당해야 하는 경우가 생기면 let을 사용한다.

 

var는 block scope를 지원하지 않기 때문에 되도록 사용하지 말자

 

+ Recent posts