tagged templates

카테고리 JavaScript

📌 template literals

템플릿 리터럴은 내장된 표현식을 허용하는 문자열 리터럴이다.

특징

  • ` 백틱이라는 기호를 사용한다.
  • ${}를 사용하여 표현식을 삽입할 수 있다.
  • 개행을 포함한다.

tagged templates

템플릿 리터럴의 발전된 형태로 태그드 템플릿이 있다.

마치 함수처럼 사용할 수 있다.

예제 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const virtualNode = {
name: "figure",
className: "tagged-template-literals",
style: { cssText: "" },
};

const styled = (strings, Node, ...value) => {
const el = strings
.slice(1, 3)
.reduce((acc, cur) => acc + cur.trim(), "")
.split(";");

el.forEach((elem, i) => {
Node.style.cssText += elem + value[i] + ";";
});
return Node;
};

let values = {
margin: "10px",
color: "#d08471",
};

const received = styled`${virtualNode}margin: ${values.margin};color: ${values.color};`;

console.log(received);
  • 마치 함수처럼 인수를 받아서 템플릿 리터럴에 넣어 반환값을 원하는 대로 지정해줄 수 있다.
  • 리액트의 StyledComponent가 태그드 템플릿을 활용하여 탄생하게 되었다.

🏓 소감

문자타입과 다른 타입을 파라미터로 사용하는 함수를 만들 때 원하는 반환값을 직관적으로 사용할 수 있어 용이해보인다.

댓글 공유

this

카테고리 JavaScript

this 키워드

메서드로 프로퍼티를 참조하고 변경하기 위해서는 우선 자신이 속한 객체를 가리키는 식별자를 참조해야만 가능한 일이다.

1
2
3
4
5
6
7
8
9
10
11
12
const circle = {
// 프로퍼티: 객체 고유의 상태 데이터
radius: 5,
// 메서드: 상태 데이터를 참조하고 조작하는 동작
getDiameter() {
// 이 메서드가 자신이 속한 객체의 프로퍼티나 다른 메서드를 참조하려면
// 자신이 속한 객체인 circle을 참조할 수 있어야 한다.
return 2 * circle.radius;
},
};

console.log(circle.getDiameter()); // 10
  • 객체 리터럴은 circle 변수에 할당되기 직전에 평가된다?
    = 할당 연산자에 의해서 피연산자를 할당해주기 위해서는 우측의 객체 리터럴이 평가된 값으로 존재해야 할당을 해줄 수 있기 때문이다.

하지만 위처럼 재귀적으로 자신이 속한 객체를 참조하는 것은 바람직하지 않다.

그 예시를 생성자 함수를 통해 설명해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
// 생성자 함수
function Circle(radius) {
// 이 시점에는 생성자 함수 자신이 생성할 인스턴스를 가리키는 식별자를 알 수 없다.
????.radius = radius;
}

Circle.prototype.getDiameter = function () {
// 이 시점에는 생성자 함수 자신이 생성할 인스턴스를 가리키는 식별자를 알 수 없다.
return 2 * ????.radius;
};

// 생성자 함수로 인스턴스를 생성하려면 먼저 생성자 함수를 정의해야 한다.
const circle = new Circle(5);
  • 생성자 함수 내부에서 프로퍼티나 메서드를 추가하기 위해서는 자신이 생성할 인스턴스를 참조할 수 있어야 하는데, 인스턴스를 생성하려면 생성자 함수가 존재해야한다.

따라서 자신이 속한 객체, 자신이 생성할 인스턴스를 가리킬 특별한 식별자가 필요하다.

this란, 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수이다. this를 통해 자신이 속한 객체나 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.

  • this는 코드 어디서든 참조할 수 있다. (전역에서도 가능)

this는 객체의 프로퍼티나 메소드를 참조하기 위한 자기 참조 변수이므로 객체의 메서드 내부 또는 생성자 함수 내부에서만 의미가 있다. 따라서 strict mode가 선언된 일반 함수 내부의 this는 undefined가 바인딩된다. (일반함수에선 필요 없다)

함수 호출방식과 this 바인딩

this에 바인딩될 값은 함수 호출 방식에 의해 동적으로 결정된다.

1. 일반 함수 호출

전역 객체에 바인딩된다.

중첩 함수 또한 일반 함수로 호출 시 함수 내부의 this는 전역 객체에 바인딩 된다.

매서드 내에서 정의된 중첩함수도 일반 함수로 호출되면 역시 전역 객체에 바인딩 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// var 키워드로 선언한 전역 변수 value는 전역 객체의 프로퍼티다.
var value = 1;
// const 키워드로 선언한 전역 변수 value는 전역 객체의 프로퍼티가 아니다.
// const value = 1;

const obj = {
value: 100,
foo() {
console.log("foo's this: ", this); // {value: 100, foo: ƒ}
console.log("foo's this.value: ", this.value); // 100

// 메서드 내에서 정의한 중첩 함수
function bar() {
console.log("bar's this: ", this); // window
console.log("bar's this.value: ", this.value); // 1
}

// 메서드 내에서 정의한 중첩 함수도 일반 함수로 호출되면 중첩 함수 내부의 this에는 전역 객체가 바인딩된다.
bar();
},
};

obj.foo();

콜백함수가 일반함수로 호출된다면 콜백함수 내부의 this에도 전역객체가 바인딩된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var value = 1;

const obj = {
value: 100,
foo() {
console.log("foo's this: ", this); // {value: 100, foo: ƒ}
// 콜백 함수 내부의 this에는 전역 객체가 바인딩된다.
setTimeout(function () {
console.log("callback's this: ", this); // window
console.log("callback's this.value: ", this.value); // 1
}, 100);
},
};

obj.foo();

하지만 메서드 내의 중첩함수와 콜백함수는 외부함수를 돕는 헬퍼 함수의 역할을 하는데 외부함수인 메서드와 중첩함수 또는 콜백함수의 this가 일치하지 않는다는 것은 중첩함수 또는 콜백함수가 헬퍼 함수로 동작하는 것을 어렵게 만든다.

화살표함수 내부에서 this

1
2
3
4
5
6
7
8
9
10
11
var value = 1;

const obj = {
value: 100,
foo() {
// 화살표 함수 내부의 this는 상위 스코프의 this를 가리킨다.
setTimeout(() => console.log(this.value), 100); // 100
},
};

obj.foo();

2. 메서드 호출

메서드 내부의 this는 메서드를 소유한 객체가 아닌 메서드를 호출한 객체에 바인딩된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const person = {
name: "Lee",
getName() {
// 메서드 내부의 this는 메서드를 호출한 객체에 바인딩된다.
return this.name;
},
};
const people = {
name: "Kim",
getName() {
return this.name;
},
};

console.log(person.getName()); // Lee
console.log(people.getName()); // Kim
  • person 객체의 getName 프로퍼티가 가리키는 함수 객체는 person 객체에 포함된 것이 아니라 독립적으로 존재하는 별도의 객체이다? 내 생각에는 this가 가리키는 것이 메서드를 소유한 객체라고 생각해도 맞지 않나?
    person 객체에 getName 프로퍼티 키가 가리키는 함수 객체를 소유하고 있는 것이 아니라 참조값을 가지므로 독립적으로 존재하는 객체를 가리키고 있는 것이 맞다.
1
2
3
4
5
6
7
8
const anotherPerson = {
name: "Kim",
};
// getName 메서드를 anotherPerson 객체의 메서드로 할당
anotherPerson.getName = person.getName;

// getName 메서드를 호출한 객체는 anotherPerson이다.
console.log(anotherPerson.getName()); // Kim
  • 새로운 객체의 프로퍼티에 person.getName 프로퍼티를 할당해줄 수 있다.

this는 getName 메서드를 호출한 객체에 바인딩된다.

프로토타입 메서드 내부에서도 마찬가지로 바인딩된다.

3. 생성자 함수 호출

생성자 함수 내부의 this에는 생성자 함수가 (미래에) 생성할 인스턴스가 바인딩 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 생성자 함수
function Circle(radius) {
// 생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스를 가리킨다.
this.radius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}

// 반지름이 5인 Circle 객체를 생성
const circle1 = new Circle(5);
// 반지름이 10인 Circle 객체를 생성
const circle2 = new Circle(10);

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20

4. Function.prototype.apply/call/bind 메서드에 의한 간접 호출

apply, call, bind 메서드는 Function.prototype의 메서드이다. 이들 메서드는 모든 함수가 상속받아 사용 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
function getThisBinding() {
return this;
}

// this로 사용할 객체
const thisArg = { a: 1 };

console.log(getThisBinding()); // window

// getThisBinding 함수를 호출하면서 인수로 전달한 객체를 getThisBinding 함수의 this에 바인딩한다.
console.log(getThisBinding.apply(thisArg)); // {a: 1}
console.log(getThisBinding.call(thisArg)); // {a: 1}

call,apply 메서드는 함수를 호출하면서 첫번째 인수로 전달한 객체를 호출한 함수의 this에 바인딩한다.

  • 위 예제에서는 getThisBinding() 함수에 인수를 전달해주지 않는다.
  • call, apply의 반환값은 호출한 함수의 반환값이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getThisBinding() {
console.log(arguments);
return this;
}

// this로 사용할 객체
const thisArg = { a: 1 };

// getThisBinding 함수를 호출하면서 인수로 전달한 객체를 getThisBinding 함수의 this에 바인딩한다.
// apply 메서드는 호출할 함수의 인수를 배열로 묶어 전달한다.
console.log(getThisBinding.apply(thisArg, [1, 2, 3]));
// Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// {a: 1}

// call 메서드는 호출할 함수의 인수를 쉼표로 구분한 리스트 형식으로 전달한다.
console.log(getThisBinding.call(thisArg, 1, 2, 3));
// Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// {a: 1}

call,apply 메서드로 함수를 호출하면서 호출한 함수에 인수를 전달해줄 수 있다.

유사배열 객체에 배열 메서드 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function convertArgsToArray() {
console.log(arguments);

// arguments 객체를 배열로 변환
// Array.prototype.slice를 인수없이 호출하면 배열의 복사본을 생성한다.
const arr = Array.prototype.slice.call(arguments);
// const arr = Array.prototype.slice.apply(arguments);
// const arr = Array.from(arguments)
console.log(arr);

return arr;
}

convertArgsToArray(1, 2, 3); // [1, 2, 3]
  • arguments 객체는 배열이 아니므로 배열 메서드를 사용할 수 없지만 apply, call 메서드를 사용하면 가능하다.

새로 나온 Array.from() 정적 메서드를 사용할 수 있다. 하지만 arguments 객체를 잘 안쓴다.

bind

1
2
3
4
5
6
7
8
9
10
11
12
function getThisBinding() {
return this;
}

// this로 사용할 객체
const thisArg = { a: 1 };

// bind 메서드는 첫 번째 인수로 전달한 thisArg로 this 바인딩이 교체된
// getThisBinding 함수를 새롭게 생성해 반환한다.
console.log(getThisBinding.bind(thisArg)); // getThisBinding
// bind 메서드는 함수를 호출하지는 않으므로 명시적으로 호출해야 한다.
console.log(getThisBinding.bind(thisArg)()); // {a: 1}
  • bind 메서드는 함수를 호출하지 않고 인수로 전달받은 객체로 this 바인딩이 교체된 함수를 새롭게 생성하여 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const person = {
name: "Lee",
foo(callback) {
// ①
setTimeout(callback, 100);
},
};

person.foo(function () {
console.log(`Hi! my name is ${this.name}.`); // ② Hi! my name is .
// 일반 함수로 호출된 콜백 함수 내부의 this.name은 브라우저 환경에서 window.name과 같다.
// 브라우저 환경에서 window.name은 브라우저 창의 이름을 나타내는 빌트인 프로퍼티이며 기본값은 ''이다.
// Node.js 환경에서 this.name은 undefined다.
});
  • person.foo의 콜백함수가 호출되기 전 1의 시점에서 this는 foo 메서드를 호출한 객체(person)를 가리킨다.
  • 그러나 person.foo의 콜백함수가 일반 함수로서 호출된 2의 시점에서 this는 전역객체 windows를 가리킨다.
  • person.foo의 콜백함수는 헬퍼함수로 person.foo를 돕는 역할을 하기 때문에 서로의 this가 같아야한다.

이 때, bind 메서드를 사용하여 this를 일치시킨다.

1
2
3
4
5
6
7
8
9
10
11
const person = {
name: "Lee",
foo(callback) {
// bind 메서드로 callback 함수 내부의 this 바인딩을 전달
setTimeout(callback.bind(this), 100);
},
};

person.foo(function () {
console.log(`Hi! my name is ${this.name}.`); // Hi! my name is Lee.
});
  • callback 함수에 this가 바인딩된 새로운 함수를 반환

코드해설

즉, foo안의 this는 person 객체를가리키는데, 콜백함수 호출하면 this가 window를 가리킨다. 그러므로 bind함수를 사용하여 foo 메서드가 가리키는 this를 callback 함수에 바인딩해줘서 콜백함수가 가리키는 this와 일치 시켜준다.

bind, call, apply, that으로 this 바인딩을 일치 시켜주는 것 보다 화살표 함수를 사용하는 것이 간편하다. 하지만 여러 가지 방식에 대해서도 알아두자.

함수 호출 방식 this 바인딩
일반 함수 호출 전역 객체
메서드 호출 메서드를 호출한 객체
생성자 함수 호출 생성자 함수가 (미래에) 생성할 인스턴스
Function.prototype.apply/call/bind 메서드에 의한 간접호출 Function.prototype.apply/call/bind 메서드에 첫번째로 전달한 객체

댓글 공유

📌 window.open()

1
window.open("https://www.w3schools.com");

새 브라우저 탭에서 해당 URL을 연다.

  • 기본값이 target="_blank" 이다.

❗️ 하이퍼 링크를 신뢰할 수 없다면…

target="\_blank" 속성이 적용되어 있다면 성능과 보안 면에서 취약점이 발생한다.

새롭게 열린 페이지가 원본 페이지와 동일한 프로세스에서 실행될 수 있다.

새로 열린 페이지는 보조 브라우징 컨텍스트이다. 새로 열린 페이지는 자기 자신을 생성한 원본 브라우징 컨텍스트를 오프너 브라우징 컨텍스트라는 이름으로 참조하고 있다.

만약 보조 브라우징 컨텍스트가 많은 JavaScript를 실행하는 경우 원본 페이지의 성능이 저하될 수 있다.

  • 단, 위와 같은 문제는 최신 브라우저에서 발생하지 않도록 처리가 되어있다.

👿 Tab nabbing(탭 내빙) - 피싱 공격

가장 큰 문제는 새롭게 열린 페이지에서 JavaScript를 통해 원본 페이지에 직접 접근이 가능해지는 문제이다.

window.opener를 이용해 원본 페이지에 접근 가능한데 이를 **탭 내빙(Tab nabbing)**이라고 부른다.

tab-nabbing

  1. 사용자가 새탭을 열었다.
  2. 해커가 새 탭에 window.opener.location을 사용하여 원본 사이트와 교묘하게 다른 링크로 바꾼다.
  3. 사용자는 본래 탭으로 돌아오니 로그인일 풀렸다고 생각하여 정보를 입력한다.
  4. 피싱 사이트가 사용자의 로그인 정보를 탈취한 후 다시 원래 링크로 되돌려 놓는다.

이러한 탭 내빙 문제는 target="_blank" 속성이 적용되어 있을 때만 발생한다.
window.open() 메서드는 기본속성이다.
그러므로 이러한 피싱 공격을 막기 위해 rel='noopener' 속성을 설정해줘야한다.

noreferer

noreferer는 noopener와 동일한 기능을 하지만 추가로 브라우저가 해당 페이지를 불러오면서 HTTP 요청을 보낼 때 referer Header를 생략하는 기능이 있다.

즉, noreferer 속성이 있으면 링크 클릭 시 해당 유입이 어디에서 발생하였는지에 대한 정보가 새 페이지에 제공되지 않는다.

  • 일반적으로는 noopener noreferer 두 속성을 같이 적어준다.

nofollow

nofollow 속성은 검색 엔진에게 링크된 웹 사이트를 보증하거나 신뢰할 수 없으니 현재 웹 사이트와 연결하지 않기를 바라는 경우에 사용한다.

  • 스팸 댓글이 등장하여 나오게 되었다.

스팸 댓글이 달린 게시물은 사이트 소유와 상관없이 평판이 내려가게 되어서 구글이 이에 대한 해결책으로 nofollow 를 제시하였다.

nofollow 속성이 설정된 링크는 크롤링하지 않고 검색 엔진에도 영향을 미치지 않는다.

그러므로 댓글이나 포럼과 같이 사용자가 참여 콘텐츠의 링크에 적합하다.

🏓 소감

allWAIs 링크 컴포넌트를 만들다가 window.location.href 속성을 사용하다가 새창을 열어주는 메서드인 window.open() 속성을 알게되었다.

그러다가 예전에 HTML 수업 때 배운 noopener, noreferer 속성이 잘 기억이 나지 않아 정리하기 위해 공부하였다.

이번 기회에 제대로 정리할 수 있어서 오래 기억에 남을 것 같다.

참고

요즘 IT - “하이퍼링크를 신뢰할 수 없다면?”

댓글 공유

이번 리액트 수업시간 때, 모듈 파일을 내보내고 불러오는 과정에서 확실히 이해하지 못한 것 같아 이 글을 작성하게 되었습니다.

📌 모듈이란?

모듈이란, 개발을 하면서 애플리케이션이 방대해짐에 따라 파일을 분리해야할 경우가 발생한다. 이 때 분리된 파일을 모듈이라고 한다.

시간이 지나 모듈이 웹 표준에 등재되면서 브라우저와 Node.js 환경에서 모듈을 사용할 수 있게 되었다.

모듈 특징

  • 모듈파일을 사용하기 위해서는 import 해온 script 파일에 type 속성이 module로 설정되어 있어야한다.
1
<script type="module" src="index.js"></script>
  • 모듈 스코프를 가진다.
  • Strict mode
  • 지연 실행된다(defer) -> 스크립트의 상대적인 순서가 유지될 수 있는 이유
  • 확장자를 꼭 적어줘야한다.

모듈은 단독적으로 사용하는 경우는 드물고 주로 웹팩(Webpack)이라는 번들러를 사용하여 모듈을 한번에 묶어 서버에 올리는 방식을 사용한다.

웹팩에 대해서는 새로운 게시물에서 다뤄보도록 하고 이제 모듈을 불러오고 내보내는 방법에 대해 알아보자.

CommonJS vs ES6 방식

1. CommonJS

  • require : 가져오기 위한 키워드
1
2
// index.js
const sayHi = require("./sayHi.js");
  • exports : 내보내기 위한 키워드
1
2
3
4
// sayHi.js
const foo = () => console.log("sayHi~!");

module.exports = foo;

2. ES6 방식

  • import : 가져오기 위한 키워드
1
2
// index.js
import foo from "./sayHi.js";
  • export
1
2
3
4
// sayHi.js
const foo = () => console.log("sayHi~!");

export default foo;

둘의 차이에 대해 알아두고 앞으로는 ES6 문법을 사용할 것이니 이에 대해서 자세히 알아보자.

export

모듈을 내보내는 방식은 다양하다.

  1. 선언부에서 내보내기
1
export const name = "loco";
  • 단, 함수나 클래스를 내보내기할 경우에는 ; (세미콜론)을 붙이지 않아야 한다.
  1. 선언부와 떨어진 곳에서 내보내기
1
2
3
const name = 'loco';

export name;

만약 여러개를 내보내고 싶다면 객체로 감싸서 내보내기 해야한다.

1
2
3
4
5
6
7
8
9
10
// say.js
const sayHi = (name) => {
console.log(`Hi~ ${name}!`);
};

const sayBye = (name) => {
console.log(`Bye~ ${name}!`);
};

export { sayHi, sayBye };

import

1
2
3
4
5
// main.js
import { sayHi, sayBye } from "./say.js";

sayHi("John"); // Hi~ John!
sayBye("selly"); // Bye~ selly!

export 할 때, 객체형태로 감싸서 내보내기를 하였으면 불러올 때도 객체형태로 감싸 이름 그대로 가져와야한다.

하지만 가져와야할 함수나 변수가 100만개라면? 일일히 언제 써주기가 힘드니 이럴 경우 * (에스터리스트)를 사용하여 모두 가져오기하여 객체처럼 사용할 수 있다.

1
2
3
4
5
// main.js
import * as greeting from "./say.js";

greeting.sayHi("Yong"); // Hi~ Yong!
greeting.sayBye("Jin"); // Bye~ Jin!

하지만 이런 경우는 드물고, 대게 어떤 대상을 가져오는지를 구체적으로 하는 것 명시하는 것이 좋다.

왜냐하면 웹팩을 사용하면 로딩속도를 높이기 위해 최적화를 하게되는데 이 경우 사용하지 않는 리소스는 삭제되기 때문이다.

import as

as를 사용하여 이름을 바꿔서 가져올 수 있다. export 에서도 가능하지만 주로 import 에서만 사용하는 것이 명시적이다.

1
2
3
4
5
// main.js
import { sayHi as hi, sayBye as bye } from "./say.js";

hi("John"); // Hi~ John!
bye("John"); // Bye~ John!

export default

모듈은 크게 2가지 종류로 나뉘게 된다.

  1. 위 처럼 say라는 함수들이 다수있는 라이브러리 형태의 모듈
  2. 개체 하나만 선언된 모듈

유틸 함수들을 내보내는 경우가 아니라면 주로 2번의 경우를 선호한다.

해당 모듈에 개체가 1개만 있다는 것을 명시적으로 구분하기 위해 export default 키워드를 사용한다.

1
2
3
import validation from "./validation.js";

const isValid = validation();
  • export default로 내보내기한 경우에 가져오기할 때는 중괄호{}가 필요없고 원하는 이름으로 가져올 수 있다.
  • 또한, 개체 한개만 내보낸 것을 가져오기 때문에 export default로 내보낼 때는 식별자나 이름없이 내보낼 수 있다.

🔥 [22.11.09 추가]

❗️ component 폴더의 index.js 모아서 다시 내보내기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// components/ToggleButton/ToggleButton.js
export function ToggleButton() {
return <button></button>;
}

// components/index.js
export * from "./Demo";
export * from "./ToggleButton";
export * from "./A11yHidden";
export * from "./Banner";

// app/App.jsx
import { ToggleButton } from "components";
export default function App() {
<div className="App">
<ToggleButton />
</div>;
}
  • ToggleButton 컴포넌트를 이름 내보내기 해주고 있다.
  • 위와 같이 components 폴더의 index.js파일에 여러 컴포넌트를 다시 내보내기로 보내줄 때, 만약 ToggleButton 컴포넌트를 기본 내보내기를 해주었다면 아래처럼 index.js에서 다시내보내기 해줄 때, {}로 감싸고 default로 받은 객체를 이름으로 설정해주고 내보내줘야한다.
1
export { default as ToggleButton } from './ToggleButton";

왜냐하면, ToggleButton 이외의 다른 컴포넌트들도 default로 내보냈기 때문에 구분을 하기 위해서 다시 내보내기 할 때, 이름을 설정해줘야한다.

이러한 불편함 때문에 이름 내보내기를 사용한다.

❗️ 화살표 함수로 내보내기시 주의사항

1
2
3
export default ({ onText, offText, on, onToggle, activeClass }) => {
return <button>...</button>;
};
  • 위와 같이 화살표 함수로 한줄로 작성하여 익명함수로 내보내고 App.jsx에서 이름을 정하여 받아서 사용하더라도 브라우저 환경에서 컴포넌트 구조를 확인하면 컴포넌트 이름이 Annonymous라고 뜬다.

🏓 소감

수업시간에 모듈을 기본 내보내기, 이름 내보내기를 사용하면서 둘의 차이가 무엇인지? 그리고 CommonJS와 ES6에서 모듈을 불러오고 내보내는 방식의 차이에 대해 확실히 알고 가는 시간이여서 한층 성장하였다고 느낀다.

댓글 공유

MVC 아키텍처 기반 TodoList V3를 만들어 보면서 느낀점을 기록해두려한다.

1. 동적 HTML vs 정적 HTML

동적 HTML은 js를 이용하여 동적으로 HTML을 render 해줘서 화면에 나타나게 하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$todoList.innerHTML = filteredTodos
.map(
({ id, content, completed }) => `
<li data-id="${id}">
<div class="view">
<input type="checkbox" class="toggle" ${completed ? "checked" : ""} />
<label>${content}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="${content}" />
</li>
`
)
.join("");

const activeTodoCount = state.todos.filter((todo) => !todo.completed).length;

if (state.todos.length === activeTodoCount) {
document.querySelector(".clear-completed").style.display = "none";
} else {
document.querySelector(".clear-completed").style.display = "block";
}

if (!activeTodoCount) {
document.querySelector(".toggle-all").checked = true;
}

위 예제를 보면서 설명해보면, 동적 HTML로 생성한 HTML의 DOM을 조작하기 위해서는 innerHTML에 할당을 해준 뒤 render 함수를 사용하여 화면에 재렌더링 하는 방식을 사용하고 있다.

반면에 정적 HTML의 DOM을 조작하기 위해서는 document.querySelector를 꼭 써줘야 하고 자바스크립트 코드를 짜는데 있어서 통일성과 일관성을 유지하기가 어렵다.

또한, HTML이 바뀌었을 때 그에 따른 요소 노드도 변경되기 때문에 유지보수가 어려워지는 단점이 있다.

이를 js가 HTML에 종속되었다고 표현하며 js가 HTML에 종속되면 될수록 일관성이 결여된 코드를 짜게되며 가독성을 떨어뜨리고 복잡성을 증가시키는 요인이 되므로, 앞으로는 js를 사용하여 동적 HTML을 생성할 것이다.

동적 HTML의 단점도 분명히 있다. 예를 들면 빈 HTML 파일에다가 동적으로 HTML을 그려넣는 것이므로 SEO(검색엔진최적화)가 떨어질 수 있다. 하지만 HTML 요소를 조작하는 작업이 많다면 동적으로 HTML을 생성해주는 것이 편리하다.

2. 동적 HTML로 MVC 아키텍처 설계

처음에 Js 파일이 읽히고 난 다음에 render 함수로 HTML이 그려지기 때문에 다음과 같이 요소노드를 취득하면 null값이 나온다.

1
document.querySelector(".todo-list"); // null

그러면 어떻게 동적인 HTML요소를 조작할 수 있을까?

=> 이벤트 위임을 통해서 이벤트를 등록할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Todos v3</title>
<link rel="stylesheet" href="css/style.css" />
<script defer src="js/app.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

HTML 파일은 다음과 같이 #root div태그만 가지고 있고 div 태그 안에 동적으로 HTML을 생성해줄 것이다. 즉, 동적으로 생성된 HTML의 요소 조작을 위해서 부모 요소 노드인 document.querySelector('#root')를 통해 동적 HTML을 조작할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
window.addEventListener("DOMContentLoaded", fetchState);

$root.addEventListener("keyup", (e) => {
if (!e.target.matches(".new-todo")) return;

const content = e.target.value.trim();
if (e.key !== "Enter" || content === "") return;

addTodo(content);
e.target.value = "";
});

일단 window가 로드되면서 서버로 부터 초기 데이터를 받아 화면에 렌더링 해주는 것부터 시작한다.

이후, 아키텍처를 고려하면서 각자 역할에 맞는 함수를 분류하여 코딩을 한다.

3. 아키텍처가 무너지면 망한다

Model에는 state를 변경해주는 함수들이 모여있고, view에는 화면에 렌더링해주는 함수들이 모여있고, controller에는 화면에서 발생한 이벤트를 이벤트 핸들러로 이벤트 객체를 생성하여 Model의 함수의 인자로 넣어 호출한다.

여기서 중요한 점은 각자의 역할이 정해져있는데 이를 절대로 무시하거나 어기는 행위를 금지한다는 것이다.

1
2
3
4
5
6
// 함수로 통일시켜주기.
$root.addEventListener("click", (e) => {
if (!e.target.matches(".filter")) return;

setState({ filterId: e.target.id });
});

controller는 이벤트 객체를 Model의 함수의 인자로 전달하여 호출하는 역할을 해야한다. 하지만 위 예제에서는 controller가 Model의 함수를 호출하는 것이 아닌 직접 setState 함수를 호출하는 모습을 볼 수 있다.

이렇게 통일성이 지켜지지 않는다면 앞으로의 코딩 협업은 무쓸모가 되므로 꼭 아키텍처를 지키면서 생각하며 코딩하는 습관을 길러야 한다.

추가로 함수와 변수의 이름은 첫 아이 이름 짓듯이 신중하게 지어줘야 한다.

4. 이벤트 핸들러 내부에 중첩된 이벤트 핸들러

1
2
3
4
5
6
7
8
9
10
11
12
// 더블클릭했을 때, editId에 상태를 관리해줘야한다.
// addEventListener 안에 또 있는것은 나눠줘야한다.
$root.addEventListener("dblclick", (e) => {
e.path[2].classList.add("editing");

e.path[2].addEventListener("keyup", (e) => {
const content = e.target.value.trim();
if (e.key !== "Enter" || content === "") return;

editTodo(content, e.path[1].dataset.id);
});
});

더블클릭하였을 때, 생겨난 input 창에 글을 수정하는 로직을 구현하기 위해 사용한 코드이다.

위와 같이 이벤트 핸들러가 중첩되어 코드를 작성하게되면 앞서 설명한 일관성에 어긋나 유지보수를 어렵게 하는 원인이 되므로 이 경우 다음과 같이 코드를 최소한의 단위로 분리해주어야 하며, 아키텍처를 고려하여 역할을 나눠서 코딩을 해줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Model

// Model에 editId를 재할당해준다. => render 함수에서는 state.editId를 보고 'editing' 클래스 추가할지 말지 확인하여 렌더링한다.
const createEditId = (id) =>
setState({
editId: +id,
});

// 편집은 한번에 한개씩만 허용하기 때문에 editId를 배열로 받지 않는다.
// 편집이 완료되었으면 editId를 초기값으로 돌려놔야 한다. render 함수에서는 state.editId를 보고 'editing' 클래스 유지할지 제거할지 판단하여 렌더링한다.
const editTodo = (content) =>
setState({
todos: state.todos.map((todo) =>
todo.id === +state.editId ? { ...todo, content } : todo
),
editId: 0,
});

// Controller

// 더블클릭하면 EditId를 생성하여 Model에 반영하는 함수를 호출한다.
$root.addEventListener("dblclick", (e) => {
createEditId(e.path[2].dataset.id);
});

// state.editId 값과 해당 요소가 같은지 비교하여 같으면 content를 매개변수로 받은 함수를 호출한다.
$root.addEventListener("keyup", (e) => {
if (!e.target.matches(".edit")) return;

const content = e.target.value.trim();
if (e.key !== "Enter" || content === "") return;

editTodo(content);
});

결론

그동안은 코드가 돌아가기만 하면 되는 것인줄 알았지만, 앞으로는 돌아가는 코드를 만들기 보다는 돌아가는 코드를 어떻게 하면 아키텍처를 잘 지키면서 각자 역할에 맞는 최소한의 단위로 구분하여 코드를 구현할지 생각하며 코딩을 하도록 해야한다는 것을 깊이 깨달은 날이었다.

댓글 공유

오늘은 MVC TodoList V2를 만들어 보면서 배웠던 내용을 정리해보는 시간을 갖겠다.

1. setState를 왜 쓰는지?

직접 라이브 코딩한다고 생각을 하면서 한줄 한줄 코딩을 해나가다가 문득 setState 함수를 사용하지 않고 render 함수로 렌더만 해주면 되지 않을까? 라는 생각이 들었다.

이후 경현님께 코드 리뷰를 부탁하여 이러한 의견을 여쭤보니 다음과 같은 이점때문에 setState 함수를 만들어 사용하는 것이 좋다는 결론이 나왔다.

  1. 관리해야할 상태 객체가 늘어나 확장성이 요구될 때 용이하다.

  2. state에 대한 에러가 발생하였을 때 에러 핸들링이 용이하다. (상황에 따라 렌더링 할지 말지 결정 가능)

  3. state가 변경되거나 재할당 되는 경우를 한곳에서 관리하여 용이하다.

2. filter 함수를 사용한 코드 리팩터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// before
let filteredTodos;
if (state.filterId === "completed") {
filteredTodos = state.todos.filter((todo) => todo.completed);
} else if (state.filterId === "active") {
filteredTodos = state.todos.filter((todo) => !todo.completed);
} else {
filteredTodos = state.todos;
}

// after
const filteredTodos = state.todos.filter((todo) => {
if (state.filterId === "completed") return todo.completed;
if (state.filterId === "active") return !todo.completed;
return todo;
});

if 문으로 filter 함수 반환값이 다르게 filteredTodos 값이 할당되는 경우라면 filter 함수 안에 로직을 넣어서 해결해보자.

=> 훨씬 더 간결해진다.

3. MVC 패턴으로 나누었다면 그에 알맞게 자신만의 역할을 하도록 분류한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// before
const addTodo = (content) => {
state.todos = [
{ id: generateNextTodoId(), content, completed: false },
...state.todos,
];
setState({ todos: state.todos });
};

// after
const addTodo = (content) =>
setState({
todos: [
{ id: generateNextTodoId(), content, completed: false },
...state.todos,
],
});

위 예제에서 addTodo 함수는 내부에서 state.todos에 새로운 값을 할당 해준뒤 state를 변경하는 함수를 호출하여 렌더링 해주고 있다.

하지만, addTodo 함수의 기능은 Model의 todos 배열에 todo 요소 하나를 추가하는 것일 뿐이므로 before 처럼 state.todos(원본)에 재할당 하는 것은 옮지 않다.

setState 함수에게 새로운 객체(상태)를 전달하여 state의 변경에 따라 화면에 렌더링 되도록 로직을 짜는 것이 더 통일성을 지킬 수 있다.

4. 함수의 매개변수를 명확하게 적어주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// before
const toggleAllCompleted = (boolean) => {
state.todos = state.todos.map((todo) => ({ ...todo, completed: boolean }));
setState({ todos: state.todos });
};

// after
const toggleAll = (checked) => {
checked
? setState({
todos: state.todos.map((todo) =>
!todo.completed ? { ...todo, completed: checked } : todo
),
})
: setState({
todos: state.todos.map((todo) =>
todo.completed ? { ...todo, completed: checked } : todo
),
});
};

before 처럼 매개변수를 boolean 으로 설정하는 것은 옳지 않다. boolean은 타입을 나타내는 것이지 매개변수에 대한 설명으로는 부적절하다.

after 코드에서는 인수로 넘어온 모든 check를 토글로 켰다 껐다 할 수 있는 함수이다.

5. 이벤트 핸들러 사용시 이벤트 객체와 선택한 요소를 명확히 이해하자

오늘 코딩할 때 이 부분에서 문제를 계속 부딪혀서 시간을 많이 할애하였다.

1
2
3
4
5
6
7
8
9
10
11
$todoList.addEventListener("dblclick", (e) => {
e.path[2].classList.add("editing");
// e.target.parentNode.parentNode.classList.add('editing');

e.path[1].nextElementSibling.addEventListener("keyup", (e) => {
const content = e.target.value.trim();
if (e.key !== "Enter" || content === "") return;

editTodo(content, e.path[1].dataset.id);
});
});

위 코드에서 ul 태그에서 더블클릭 이벤트 발생하면 div가 안보이게되고 input이 보이게 된다.

이 때, 내가 어떤 요소를 선택하여 어떤 타입의 이벤트를 등록하였고 그 타입에 따른 이벤트 객체는 무엇이 나오는지를 명확히 이해하고 코딩을 하자.

헷갈린다면 console.log()를 찍어보면서 확인하자. 그림을 그리거나 HTML 구조와 비교해가면서 이해하면 수월하다.

소감

오늘 MVC TodoList V2를 만들어보면서 아침 10시부터 저녁 12시까지 이거만 집중해봤는데, 확실히 아직 익숙하지 않아서 버벅거리는 거 같다고 생각이 든다. 이해가 안되는 부분은 없지만 손으로 코딩이 술술 나오지가 않아서 어렵게 느껴졌다. 앞으로도 더 손 코딩을 많이 해보는 습관을 길러야 겠다.

댓글 공유

MVC 패턴을 적용시킨 TodoList

MVC 패턴을 적용 시킨 TodoList를 달달 외울 정도로 손에 익혀두자.

이를 기본으로 앞으로 애플리케이션을 만들때 두고두고 유용하게 쓰일 것이다.

HTML, CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MVC - TodoList</title>
<style>
.todo-list {
list-style-type: none;
padding-inline-start: 0;
}

.todo-filters {
display: flex;
gap: 15px;
list-style-type: none;
padding-inline-start: 0;
}

.todo-list input:checked + span {
text-decoration: line-through;
}

.active {
text-decoration: underline;
}
</style>
</head>

<body>
<input type="text" class="todo-input" />
<ul class="todo-list"></ul>
<ul class="todo-filters">
<li id="all" class="active">All</li>
<li id="completed">Completed</li>
<li id="active">Active</li>
</ul>
</body>
</html>
  1. 우선 정적인 요소와 동적인 요소를 구분지어서 HTML 구조를 설계한다.

동적인 todo 요소를 추가할 것이고 그 외에 항목들은 정적인 요소들이다.

Javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
let todos = [];

const $todoInput = document.querySelector(".todo-input");
const $todoList = document.querySelector(".todo-list");

const generateNextTodoId = () =>
Math.max(...todos.map((todo) => todo.id), 0) + 1;

const render = (todos) => {
$todoList.innerHTML = todos
.map(
({ id, content, completed }) =>
`<li id="${id}">
<input type="checkbox" ${completed ? "checked" : ""} />
<span>${content}</span>
<button>X</button>
</li>`
)
.join("");
};

const setTodos = (newTodos) => {
todos = newTodos;
render(todos);
};

const fetchTodos = () => {
setTodos([
{ id: 3, content: "일찍 일어나기", completed: false },
{ id: 2, content: "공부하기", completed: false },
{ id: 1, content: "저녁먹기", completed: false },
]);
};

const addTodo = (content) => {
todos = [{ id: generateNextTodoId(), content, completed: false }, ...todos];
setTodos(todos);
};

const toggleCompleted = (id) => {
todos = todos.map((todo) =>
todo.id === +id ? { ...todo, completed: !todo.completed } : todo
);
setTodos(todos);
};

const removeTodo = (id) => {
todos = todos.filter((todo) => todo.id !== +id);
setTodos(todos);
};

window.addEventListener("DOMContentLoaded", () => {
fetchTodos();
});

$todoInput.addEventListener("keyup", (e) => {
const content = $todoInput.value.trim();
if (e.key !== "Enter" || content === "") return;
addTodo(content);
$todoInput.value = "";
});

$todoList.addEventListener("change", (e) => {
toggleCompleted(e.target.parentNode.id);
});

$todoList.addEventListener("click", (e) => {
if (!e.target.matches("button")) return;

removeTodo(e.target.parentNode.id);
});
  1. 화면을 나타내는 부분을 View

  2. 상태(데이터)와 상태를 변경하는 메서드를 Model

  3. 화면으로 부터 받은 이벤틑 객체를 가지고 Model의 함수를 호출하여 상태를 변경하는 Controller

크게 3가지 역할로 구분하여 애플리케이션을 바라보자.

단, 너무 이 틀에 강박관념을 갖지 말길 바란다. 이러한 큰 틀이 있는 것일 뿐 100% 이 틀에 맞게 구분하는 것을 불가능하다.

댓글 공유

비동기 이해하기 (연습)

이번시간에는 비동기 처리 결과를 이용하는 예제를 따라쳐보면서 익숙해지도록 해보자.

목적

API를 활용하여 서버에서 데이터를 가져와 화면에 렌더링하기

코드 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>

<body>
<ul></ul>
<script>
const url = "https://api.artic.edu/api/v1/artworks";
const render = (items) => {
document.querySelector("ul").innerHTML = items
.map((item) => `<li>${item.title}</li>`)
.join("");
};
const get = () => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();

xhr.addEventListener("load", () => {
if (xhr.status === 200) {
render(JSON.parse(xhr.response).data);
} else {
console.error("Status Error occured");
}
});
};
</script>
</body>
</html>

서버에 XMLHttpRequest 요청을 보내 데이터 응답을 받았다.

이 과정을 다시 살펴보면 비동기 코드의 처리 결과를 render() 함수의 인수로 전달해주면서 비동기 처리 결과를 다루는 모습을 볼 수 있다.

만약에 비동기 처리 결과를 단순히 렌더해주는 것만이 아니라, 데이터의 조작이나 어떤 로직으로 처리가 많아지면 그 때마다 함수의 인수로 전달해줘야 하기 때문에 콜백 헬 현상이 발생하여 가독성과 복잡성이 떨어지게된다.

콜백함수를 이용한 비동기 처리 결과 다루기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const url = "https://api.artic.edu/api/v1/artworks";
const render = (items) =>
items.map((item) => `<li>${item.title}</li>`).join("");
const get = (url, callback) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();

xhr.addEventListener("load", () => {
if (xhr.status === 200) {
callback(JSON.parse(xhr.response).data);
} else {
console.error(`${xhr.status} ${xhr.statusText}`);
}
});
};
get(url, (param) => {
document.querySelector("ul").innerHTML = render(param);
});

위와 같이 비동기 처리 결과를 렌더해주는 로직 하나만 해주고 있어서 콜백 헬이 발생하지 않는다.

만약 데이터를 받아와서 수정을 거치고 삭제도 하고 여러가지 로직을 다루게 되면 그 때마다 함수의 인수로 전달해줘야 하기 때문에 코드가 가로로 길어지게 된다.

또한, 콜백함수들에서 에러가 발생했을 경우 에러처리가 까다로워진다.

에러는 caller 즉, 함수 호출자(콜 스택하위)방향으로 전파가 된다. 비동기 함수는 호출자가 브라우저이므로 브라우저가 콜백함수를 호출할 때에는 이벤트 루프가 콜 스택에 비었을 때, 태스크 큐에서 콜 스택으로 콜백함수를 이동시켜(호출)주기 때문에 콜 스택 하위로 에러 전파가 발생하지 않아서 에러가 발생한 위치에서만 에러를 캐치해야한다는 단점이 있다.

프로미스로 비동기 처리 결과 다루기

프로미스는 객체의 완료 상태와 값을 저장한 객체이다. 이 때, 값에는 비동기 처리 성공시 resolve 함수의 인수로 전달되어 값이 저장되고 실패이 reject 함수의 인수로 전달된 에러가 저장된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const url = "https://api.artic.edu/api/v1/artworks";
const render = (items) =>
items.map((item) => `<li>${item.title}</li>`).join("");
const get = (url) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();

xhr.addEventListener("load", () => {
if (xhr.status === 200) {
resolve(render(JSON.parse(xhr.response).data));
} else {
reject(xhr.status);
}
});
});
const promise = get(url);
promise
.then((result) => {
document.querySelector("ul").innerHTML = result;
})
.catch((err) => {
console.error(err);
});

get 함수가 프로미스 객체를 반환하고 있다. 반환한 프로미스는 콜백함수를 인수로 받는데 그 콜백함수 안에서 로직을 구현하고 콜백함수의 인수로 2개의 함수를 전달한다. 성공했을 경우 호출함 함수 resolve 함수와 실패했을 때 호출할 함수 reject 함수를 구현한다.

get 함수를 호출하여 반환된 프로미스 객체를 후속 처리 메서드 then, catch가 이어서 받아서 처리를 해준다.

  • then 메서드 : 프로미스 객체의 상태가 pending 상태에서 fulfilled로 변경되면 resolve 함수의 인수를 인수로 전달 받아 콜백함수를 호출한다.

  • catch 메서드 : 프로미스 객체의 상태가 pending 상태에서 rejected로 변경되면 reject 함수의 인수를 인수로 전달받아 콜백함수를 호출한다.

catch 메서드를 사용하면 catch 이전의 구간에서 발생한 에러를 캐치할 수 있다.

async, await로 비동기 처리 결과 다루기

ES6에서 새로 생겨난 async, await를 사용하면 마치 동기 처리 방식처럼 비동기 처리결과를 다룰 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const url = "https://api.artic.edu/api/v1/artworks";
const render = (items) =>
items.map((item) => `<li>${item.title}</li>`).join("");
const get = (url) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();

xhr.addEventListener("load", () => {
if (xhr.status === 200) {
resolve(render(JSON.parse(xhr.response).data));
} else reject(xhr.status);
});
});
(async () => {
const titleData = await get(url);
document.querySelector("ul").innerHTML = titleData;
})();
  • await는 반드시 async 키워드로 선언된 함수 안에서 사용해야한다.

    단, Node.js 애플리케이션 환경에서 함수 레벨이 아닌 최상위 레벨에서도 await를 사용할 수 있다.

1
2
3
4
// package.json
{
"type": "module"
}

package.json 파일의 type을 module로 바꿔주면 독자적인 스코프인 모듈 스코프를 갖게되어 최상위 레벨에서도 await를 사용할 수 있다.

  • async를 사용하면 에러전파가 되어 에러처리가 용이하다.

제너레이터 함수와 연관지어 생각해보면 await 키워드를 만나면 일시 정지하고 제어권을 함수 외부로 이동시켜 동기 처리르 하다가 전역 코드가 다 실행되고 다시 일시 정지 했던 await 부분으로 돌아와서 프로미스 객체의 완료 상태가 settled로 바뀌면 코드를 실행하고 다음 코드 라인으로 넘어간다.

즉, 프로미스 객체의 완료 상태가 pending이면 다음 코드를 실행하지 않고 계속 대기한다.

결론

비동기를 동기처럼 다룰 수 있는 async,await를 사용하자.

댓글 공유

CORS

카테고리 JavaScript

CORS란?

크롬, 사파리 같은 브라우저에서 보안상의 이슈때문에 모든 웹 사이트는 같은 출처에 대한 데이터 요청은 허락하지만(SOP, 동일 출처 정책), 다른 출처로 데이터를 요청할 경우 특별한 규칙에 따라 허락을 받아야한다.

Cross Origin Resource Sharing

교차 출처 정보 공유에 대한 정책으로, 브라우저가 출처가 불분명한 응답을 막고 있는 것을 풀어주는 역할을 한다.

예시

내가 은행 사이트 서버에 요청을 보내서 은행 홈페이지에 로그인을 하였다. 그러면 브라우저 쿠키에 사용자의 인증정보 및 쿠키가 저장되어 있는데, 해커가 어떤 사이트 링크를 내게 보내서 내가 그 링크를 클릭하여 앞서 말한 인증 정보, 쿠키를 서버로 가져오는 Script 코드가 포함된 리소스를 응답으로 보내서 내가 만약에 그 응답을 받게 된다면, 해커가 나의 인증 정보와 쿠키를 가져갈 수 있게 된다.

이러한 보안상의 이슈를 방지하기 위해 브라우저가 동일한 출처의 요청이 아니라면 응답을 막아주는 것이다.

또한 쿠키를 못읽게 자바스크립트 코드로 만들어줘야한다.

즉, CORS는 다른 출처 간의 리소스를 공유할 수 있도록 하는 정책이다.

  • 여기서 말하는 출처란, 보내고 받는 위치 즉, 웹 사이트랑 API 주소이다.
  • 리소스는 주고 받는 데이터를 말한다.

내가 만든 사이트와 어떤 API라는 서로 다른 출처끼리 정보교환이 가능하려면 CORS 정책을 지켜야 한다는 말이다.

CORS 과정

요청을 받는 서버쪽에서 허락할 웹사이트를 미리 명시해줘야한다.

Simple Request (GET, POST 방식일 때 사용)

  1. 브라우저는 다른 출처끼리의 요청을 보낼 때에는 요청에 Origin 이라는 header를 추가한다.
1
https://이주의사이트.com:5000
  • Origin은 요청하는 쪽의 scheme, 도메인, 포트가 담겨있다.
    • scheme은 요청할 자원에 접근할 방법(Http, FTP, telnet…) = 프로토콜
  • https - scheme
  • 이주의사이트.com - 도메인
  • :5000 - 포트
  1. 요청을 받은 API 서버는 응답의 헤더에 지정된 ACAO(Access Control Allow Origin) 정보를 실어서 보낸다.

    • ACAO 정보에는 미리 명시된 URL들이 들어가있다.
  2. 브라우저가 ACAO정보가 담긴 응답과 요청의 Origin을 비교하여 동일하면 허락해준다.

  3. 만약 동일하지 않아 허락못받으면 응답만 못받아온다. 빨간색 에러 발생

추가로 토큰과 같은 사용자 식별 정보가 담긴 요청에 대해서는 더 엄격한데 요청의 옵션에 credentials 항목을 true로 세팅해줘야하며, 받는 쪽에서도 아무 출처나 다 된다는 의미의 와일드 카드(*)가 아니라 보내는 쪽의 출처와 웹페이지 주소를 명확히 명시하고 Access Control Allow Credentials 항목을 true로 맞춰줘야 한다.

Preflighted (PUT, DELETE 방식일 때 사용)

Preflight 요청을 먼저 보내서 그에 대한 서버의 응답을 보고 안전한지 먼저 확인한다. 여기서 서버의 허락이 받아야지만 본 요청을 보낼 수 있다.

본 요청에 대한 과정은 심플리퀘스트와 동일하다.

  • Origin, credentials, method 가 담긴 요청

⇒ 서버의 데이터에 영향을 줄 수 있는 요청이기 때문에 요청 보내기 전에 먼저 허용 여부를 검증해줘야 한다.

댓글 공유

setTimeout 타이머 함수는 일정 시간이 경과된 이후 콜백 함수가 호출되도록 타이머를 생성한다. setTimeout 함수가 생성한 타이머가 만료되면 콜백함수가 호출된다.

  • 타이머 함수는 ECMAScript 사양에 정의된 빌트인 함수가 아니라 호스트 객체이다.
  • setTimeout 함수가 생성한 타이머는 1번 동작하고 콜백함수도 1번 호출된다.

setTimeout 함수

두번째 인수로 전달받은 시간(ms)으로 단 한번 동작하는 타이머를 생성한다. 타이머 만료되면 첫번째 인수로 전달받은 콜백함수가 호출된다.

1
2
3
4
5
6
7
8
9
10
11
const timeoutId = setTimeout(func[, delay,param1, param2,...]

// 1초(1000ms) 후 타이머가 만료되면 콜백 함수가 호출된다.
setTimeout(() => console.log('Hi!'), 1000);

// 1초(1000ms) 후 타이머가 만료되면 콜백 함수가 호출된다.
// 이때 콜백 함수에 'Lee'가 인수로 전달된다.
setTimeout(name => console.log(`Hi! ${name}.`), 1000, 'Lee');

// 두 번째 인수(delay)를 생략하면 기본값 0이 지정된다.
setTimeout(() => console.log('Hello!'));
  • 콜백함수에 전달할 인수가 있다면 세번째 이후의 인수로 전달할 수 있다.
  • setTimeout 함수는 생성된 타이머를 식별할 수 있는 고유한 타이머 id를 반환한다.
  • setTimeout 함수가 반환한 타이머 id를 clearTimeout 함수의 인수로 전달하여 타이머를 취소한다.

setTimeout 함수에 대한 오해

흔히들 setTimeout 함수를 인수로 전달해준 딜레이 시간이 지난 후에 콜백함수를 호출해준다고 알고 있다.

하지만 이 말에는 정확히 짚고 넘어가야할 부분이 있다.

setTimeout 함수의 첫번째 인수로 넘겨준 콜백함수는 setTimeout 함수가 호출해주는 것인가?

=> 그렇지 않다.

1
2
setTimeout(() => console.log("타이머"), 0);
console.log("호출해주세요.");

만약 위 코드에서 setTimeout 함수가 콜백함수를 호출하는 것이라면 setTimeout 함수의 실행 컨텍스트가 제거되지 않고 남아 있어 “호출해주세요”라는 메시지가 콘솔창에 출력되지 않고 Blocking 현상이 발생해야한다.

하지만 그렇지 않다. 그 이유는 setTimeout 함수는 타이머를 생성하고 timeId를 반환한 후 브라우저에게 타이머 계산과 콜백함수 호출을 위임하고 setTimeout 함수의 실행 컨텍스트가 종료된다.

쉽게 말해서 setTimeout 함수가 브라우저에게 일정 시간이 지나면 콜백함수를 호출해주세요~ 라고 위임하고 종료되는 것이다.

즉, setTimeout 함수의 콜백함수는 브라우저가 호출하는 것이다.

비동기 함수

자바스크립트 엔진의 콜 스택은 싱글 스레드이기 때문에 한번에 한가지 일 밖에 할 수 없다. 그리하여 시간이 오래 걸리는 작업을 하게된다면 그 작업이 끝날 때 까지 다음 코드가 실행되지 못하는 Blocking(블로킹) 현상이 발생한다.

시간이 오래걸리는 작업이나 setTimeout 함수의 콜백함수, addEventListener의 이벤트 핸들러 같은 함수는 개발자가 호출하지 않고 브라우저가 호출한다. 이러한 코드들을 포함하고 있는 함수를 비동기 함수라고 한다.

자바스크립트는 Blocking(블로킹) 현상을 해결하면서 동시성을 추구하기 위해 콜 스택, 태스크 큐, 이벤트 루프 구조를 생성하게 되었다.

eventLoop

위 이미지를 보면서 아까의 코드를 이해해보면,

  1. 전역 실행 컨텍스트가 생성되어 콜 스택에 쌓인다.
  2. setTimeout 함수 실행 컨텍스트가 생성되어 콜 스택에 푸시된다.
  3. setTimeout 함수는 두번째 인수로 전달받은 딜레이를 가진 타이머를 생성하고 브라우저에게 타이머 계산과 첫번째 인수로 전달받은 콜백함수를 호출할 것을 위임하고 timeId를 반환하며 종료되고 실행 컨텍스트 스택에서 pop 되어 종료된다.
  4. 브라우저는 타이머를 계산하여 만료되면 태스크 큐에 콜백함수를 전달한다. 동시에 콜 스택에서는 console.log 함수 실행 컨텍스트가 생성된다.
  5. 이벤트 루프는 콜 스택과 태스크 큐를 확인하면서 콜 스택이 비워지면 태스크 큐의 작업을 콜 스택으로 푸시하는 역할을 한다. 즉, console.log 함수 실행 컨텍스트가 제거되고 전역 실행 컨텍스트가 제거되기 전까지는 태스크 큐에 있는 콜백함수가 콜 스택으로 푸시될 수 없다.
  6. console.log 함수가 종료되어 실행 컨텍스트가 pop되어 제거되고 전역 실행 컨텍스트도 pop되어 제거되면 이벤트 루프가 콜 스택이 비어있는 것을 확인하여 태스크 큐의 콜백함수를 콜 스택으로 푸시한다.
  7. 브라우저가 호출하여 콜백함수의 실행 컨텍스트가 생성되고 콜백함수를 실행한 뒤 종료되면 실행 컨텍스트 스택에서 pop되어 제거되고 코드가 종료된다.

7번에서 브라우저가 콜백함수를 호출한다는 의미는 태스크 큐에 있던 콜백함수를 콜 스택으로 이동시켜준다는 의미이다.

다음 시간에는 비동기 함수의 callback 패턴과 promise 패턴, async, await에 대해서 알아보자.

참고자료
https://velog.io/@jjunyjjuny/%EB%B2%88%EC%97%AD-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%8B%9C%EA%B0%81%ED%99%94-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84

댓글 공유

loco9939

author.bio


author.job