메서드로 프로퍼티를 참조하고 변경하기 위해서는 우선 자신이 속한 객체를 가리키는 식별자를 참조해야만 가능한 일이다.
1 2 3 4 5 6 7 8 9 10 11 12
const circle = { // 프로퍼티: 객체 고유의 상태 데이터 radius: 5, // 메서드: 상태 데이터를 참조하고 조작하는 동작 getDiameter() { // 이 메서드가 자신이 속한 객체의 프로퍼티나 다른 메서드를 참조하려면 // 자신이 속한 객체인 circle을 참조할 수 있어야 한다. return2 * circle.radius; }, };
console.log(circle.getDiameter()); // 10
객체 리터럴은 circle 변수에 할당되기 직전에 평가된다? = 할당 연산자에 의해서 피연산자를 할당해주기 위해서는 우측의 객체 리터럴이 평가된 값으로 존재해야 할당을 해줄 수 있기 때문이다.
하지만 위처럼 재귀적으로 자신이 속한 객체를 참조하는 것은 바람직하지 않다.
그 예시를 생성자 함수를 통해 설명해보자
1 2 3 4 5 6 7 8 9 10 11 12 13
// 생성자 함수 functionCircle(radius) { // 이 시점에는 생성자 함수 자신이 생성할 인스턴스를 가리키는 식별자를 알 수 없다. ????.radius = radius; }
Circle.prototype.getDiameter = function () { // 이 시점에는 생성자 함수 자신이 생성할 인스턴스를 가리키는 식별자를 알 수 없다. return2 * ????.radius; };
// 생성자 함수로 인스턴스를 생성하려면 먼저 생성자 함수를 정의해야 한다. const circle = newCircle(5);
생성자 함수 내부에서 프로퍼티나 메서드를 추가하기 위해서는 자신이 생성할 인스턴스를 참조할 수 있어야 하는데, 인스턴스를 생성하려면 생성자 함수가 존재해야한다.
따라서 자신이 속한 객체, 자신이 생성할 인스턴스를 가리킬 특별한 식별자가 필요하다.
this란, 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수이다. this를 통해 자신이 속한 객체나 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.
this는 코드 어디서든 참조할 수 있다. (전역에서도 가능)
this는 객체의 프로퍼티나 메소드를 참조하기 위한 자기 참조 변수이므로 객체의 메서드 내부 또는 생성자 함수 내부에서만 의미가 있다. 따라서 strict mode가 선언된 일반 함수 내부의 this는 undefined가 바인딩된다. (일반함수에선 필요 없다)
하지만 메서드 내의 중첩함수와 콜백함수는 외부함수를 돕는 헬퍼 함수의 역할을 하는데 외부함수인 메서드와 중첩함수 또는 콜백함수의 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는 메서드를 호출한 객체에 바인딩된다. returnthis.name; }, }; const people = { name: "Kim", getName() { returnthis.name; }, };
console.log(person.getName()); // Lee console.log(people.getName()); // Kim
person 객체의 getName 프로퍼티가 가리키는 함수 객체는 person 객체에 포함된 것이 아니라 독립적으로 존재하는 별도의 객체이다? 내 생각에는 this가 가리키는 것이 메서드를 소유한 객체라고 생각해도 맞지 않나? person 객체에 getName 프로퍼티 키가 가리키는 함수 객체를 소유하고 있는 것이 아니라 참조값을 가지므로 독립적으로 존재하는 객체를 가리키고 있는 것이 맞다.
arguments 객체는 배열이 아니므로 배열 메서드를 사용할 수 없지만 apply, call 메서드를 사용하면 가능하다.
새로 나온 Array.from() 정적 메서드를 사용할 수 있다. 하지만 arguments 객체를 잘 안쓴다.
bind
1 2 3 4 5 6 7 8 9 10 11 12
functiongetThisBinding() { returnthis; }
// 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 바인딩을 일치 시켜주는 것 보다 화살표 함수를 사용하는 것이 간편하다. 하지만 여러 가지 방식에 대해서도 알아두자.
위와 같이 components 폴더의 index.js파일에 여러 컴포넌트를 다시 내보내기로 보내줄 때, 만약 ToggleButton 컴포넌트를 기본 내보내기를 해주었다면 아래처럼 index.js에서 다시내보내기 해줄 때, {}로 감싸고 default로 받은 객체를 이름으로 설정해주고 내보내줘야한다.
HTML 파일은 다음과 같이 #root div태그만 가지고 있고 div 태그 안에 동적으로 HTML을 생성해줄 것이다. 즉, 동적으로 생성된 HTML의 요소 조작을 위해서 부모 요소 노드인 document.querySelector('#root')를 통해 동적 HTML을 조작할 수 있다.
일단 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");
위 코드에서 ul 태그에서 더블클릭 이벤트 발생하면 div가 안보이게되고 input이 보이게 된다.
이 때, 내가 어떤 요소를 선택하여 어떤 타입의 이벤트를 등록하였고 그 타입에 따른 이벤트 객체는 무엇이 나오는지를 명확히 이해하고 코딩을 하자.
헷갈린다면 console.log()를 찍어보면서 확인하자. 그림을 그리거나 HTML 구조와 비교해가면서 이해하면 수월하다.
소감
오늘 MVC TodoList V2를 만들어보면서 아침 10시부터 저녁 12시까지 이거만 집중해봤는데, 확실히 아직 익숙하지 않아서 버벅거리는 거 같다고 생각이 든다. 이해가 안되는 부분은 없지만 손으로 코딩이 술술 나오지가 않아서 어렵게 느껴졌다. 앞으로도 더 손 코딩을 많이 해보는 습관을 길러야 겠다.
위와 같이 비동기 처리 결과를 렌더해주는 로직 하나만 해주고 있어서 콜백 헬이 발생하지 않는다.
만약 데이터를 받아와서 수정을 거치고 삭제도 하고 여러가지 로직을 다루게 되면 그 때마다 함수의 인수로 전달해줘야 하기 때문에 코드가 가로로 길어지게 된다.
또한, 콜백함수들에서 에러가 발생했을 경우 에러처리가 까다로워진다.
에러는 caller 즉, 함수 호출자(콜 스택하위)방향으로 전파가 된다. 비동기 함수는 호출자가 브라우저이므로 브라우저가 콜백함수를 호출할 때에는 이벤트 루프가 콜 스택에 비었을 때, 태스크 큐에서 콜 스택으로 콜백함수를 이동시켜(호출)주기 때문에 콜 스택 하위로 에러 전파가 발생하지 않아서 에러가 발생한 위치에서만 에러를 캐치해야한다는 단점이 있다.
프로미스로 비동기 처리 결과 다루기
프로미스는 객체의 완료 상태와 값을 저장한 객체이다. 이 때, 값에는 비동기 처리 성공시 resolve 함수의 인수로 전달되어 값이 저장되고 실패이 reject 함수의 인수로 전달된 에러가 저장된다.
get 함수가 프로미스 객체를 반환하고 있다. 반환한 프로미스는 콜백함수를 인수로 받는데 그 콜백함수 안에서 로직을 구현하고 콜백함수의 인수로 2개의 함수를 전달한다. 성공했을 경우 호출함 함수 resolve 함수와 실패했을 때 호출할 함수 reject 함수를 구현한다.
get 함수를 호출하여 반환된 프로미스 객체를 후속 처리 메서드 then, catch가 이어서 받아서 처리를 해준다.
then 메서드 : 프로미스 객체의 상태가 pending 상태에서 fulfilled로 변경되면 resolve 함수의 인수를 인수로 전달 받아 콜백함수를 호출한다.
catch 메서드 : 프로미스 객체의 상태가 pending 상태에서 rejected로 변경되면 reject 함수의 인수를 인수로 전달받아 콜백함수를 호출한다.
catch 메서드를 사용하면 catch 이전의 구간에서 발생한 에러를 캐치할 수 있다.
async, await로 비동기 처리 결과 다루기
ES6에서 새로 생겨난 async, await를 사용하면 마치 동기 처리 방식처럼 비동기 처리결과를 다룰 수 있다.
단, Node.js 애플리케이션 환경에서 함수 레벨이 아닌 최상위 레벨에서도 await를 사용할 수 있다.
1 2 3 4
// package.json { "type":"module" }
package.json 파일의 type을 module로 바꿔주면 독자적인 스코프인 모듈 스코프를 갖게되어 최상위 레벨에서도 await를 사용할 수 있다.
async를 사용하면 에러전파가 되어 에러처리가 용이하다.
제너레이터 함수와 연관지어 생각해보면 await 키워드를 만나면 일시 정지하고 제어권을 함수 외부로 이동시켜 동기 처리르 하다가 전역 코드가 다 실행되고 다시 일시 정지 했던 await 부분으로 돌아와서 프로미스 객체의 완료 상태가 settled로 바뀌면 코드를 실행하고 다음 코드 라인으로 넘어간다.
즉, 프로미스 객체의 완료 상태가 pending이면 다음 코드를 실행하지 않고 계속 대기한다.
크롬, 사파리 같은 브라우저에서 보안상의 이슈때문에 모든 웹 사이트는 같은 출처에 대한 데이터 요청은 허락하지만(SOP, 동일 출처 정책), 다른 출처로 데이터를 요청할 경우 특별한 규칙에 따라 허락을 받아야한다.
Cross Origin Resource Sharing
교차 출처 정보 공유에 대한 정책으로, 브라우저가 출처가 불분명한 응답을 막고 있는 것을 풀어주는 역할을 한다.
예시
내가 은행 사이트 서버에 요청을 보내서 은행 홈페이지에 로그인을 하였다. 그러면 브라우저 쿠키에 사용자의 인증정보 및 쿠키가 저장되어 있는데, 해커가 어떤 사이트 링크를 내게 보내서 내가 그 링크를 클릭하여 앞서 말한 인증 정보, 쿠키를 서버로 가져오는 Script 코드가 포함된 리소스를 응답으로 보내서 내가 만약에 그 응답을 받게 된다면, 해커가 나의 인증 정보와 쿠키를 가져갈 수 있게 된다.
이러한 보안상의 이슈를 방지하기 위해 브라우저가 동일한 출처의 요청이 아니라면 응답을 막아주는 것이다.
또한 쿠키를 못읽게 자바스크립트 코드로 만들어줘야한다.
즉, CORS는 다른 출처 간의 리소스를 공유할 수 있도록 하는 정책이다.
여기서 말하는 출처란, 보내고 받는 위치 즉, 웹 사이트랑 API 주소이다.
리소스는 주고 받는 데이터를 말한다.
내가 만든 사이트와 어떤 API라는 서로 다른 출처끼리 정보교환이 가능하려면 CORS 정책을 지켜야 한다는 말이다.
CORS 과정
요청을 받는 서버쪽에서 허락할 웹사이트를 미리 명시해줘야한다.
Simple Request (GET, POST 방식일 때 사용)
브라우저는 다른 출처끼리의 요청을 보낼 때에는 요청에 Origin 이라는 header를 추가한다.
요청을 받은 API 서버는 응답의 헤더에 지정된 ACAO(Access Control Allow Origin) 정보를 실어서 보낸다.
ACAO 정보에는 미리 명시된 URL들이 들어가있다.
브라우저가 ACAO정보가 담긴 응답과 요청의 Origin을 비교하여 동일하면 허락해준다.
만약 동일하지 않아 허락못받으면 응답만 못받아온다. 빨간색 에러 발생
추가로 토큰과 같은 사용자 식별 정보가 담긴 요청에 대해서는 더 엄격한데 요청의 옵션에 credentials 항목을 true로 세팅해줘야하며, 받는 쪽에서도 아무 출처나 다 된다는 의미의 와일드 카드(*)가 아니라 보내는 쪽의 출처와 웹페이지 주소를 명확히 명시하고 Access Control Allow Credentials 항목을 true로 맞춰줘야 한다.
Preflighted (PUT, DELETE 방식일 때 사용)
Preflight 요청을 먼저 보내서 그에 대한 서버의 응답을 보고 안전한지 먼저 확인한다. 여기서 서버의 허락이 받아야지만 본 요청을 보낼 수 있다.
본 요청에 대한 과정은 심플리퀘스트와 동일하다.
Origin, credentials, method 가 담긴 요청
⇒ 서버의 데이터에 영향을 줄 수 있는 요청이기 때문에 요청 보내기 전에 먼저 허용 여부를 검증해줘야 한다.
만약 위 코드에서 setTimeout 함수가 콜백함수를 호출하는 것이라면 setTimeout 함수의 실행 컨텍스트가 제거되지 않고 남아 있어 “호출해주세요”라는 메시지가 콘솔창에 출력되지 않고 Blocking 현상이 발생해야한다.
하지만 그렇지 않다. 그 이유는 setTimeout 함수는 타이머를 생성하고 timeId를 반환한 후 브라우저에게 타이머 계산과 콜백함수 호출을 위임하고 setTimeout 함수의 실행 컨텍스트가 종료된다.
쉽게 말해서 setTimeout 함수가 브라우저에게 일정 시간이 지나면 콜백함수를 호출해주세요~ 라고 위임하고 종료되는 것이다.
즉, setTimeout 함수의 콜백함수는 브라우저가 호출하는 것이다.
비동기 함수
자바스크립트 엔진의 콜 스택은 싱글 스레드이기 때문에 한번에 한가지 일 밖에 할 수 없다. 그리하여 시간이 오래 걸리는 작업을 하게된다면 그 작업이 끝날 때 까지 다음 코드가 실행되지 못하는 Blocking(블로킹) 현상이 발생한다.
시간이 오래걸리는 작업이나 setTimeout 함수의 콜백함수, addEventListener의 이벤트 핸들러 같은 함수는 개발자가 호출하지 않고 브라우저가 호출한다. 이러한 코드들을 포함하고 있는 함수를 비동기 함수라고 한다.
자바스크립트는 Blocking(블로킹) 현상을 해결하면서 동시성을 추구하기 위해 콜 스택, 태스크 큐, 이벤트 루프 구조를 생성하게 되었다.
위 이미지를 보면서 아까의 코드를 이해해보면,
전역 실행 컨텍스트가 생성되어 콜 스택에 쌓인다.
setTimeout 함수 실행 컨텍스트가 생성되어 콜 스택에 푸시된다.
setTimeout 함수는 두번째 인수로 전달받은 딜레이를 가진 타이머를 생성하고 브라우저에게 타이머 계산과 첫번째 인수로 전달받은 콜백함수를 호출할 것을 위임하고 timeId를 반환하며 종료되고 실행 컨텍스트 스택에서 pop 되어 종료된다.
브라우저는 타이머를 계산하여 만료되면 태스크 큐에 콜백함수를 전달한다. 동시에 콜 스택에서는 console.log 함수 실행 컨텍스트가 생성된다.
이벤트 루프는 콜 스택과 태스크 큐를 확인하면서 콜 스택이 비워지면 태스크 큐의 작업을 콜 스택으로 푸시하는 역할을 한다. 즉, console.log 함수 실행 컨텍스트가 제거되고 전역 실행 컨텍스트가 제거되기 전까지는 태스크 큐에 있는 콜백함수가 콜 스택으로 푸시될 수 없다.
console.log 함수가 종료되어 실행 컨텍스트가 pop되어 제거되고 전역 실행 컨텍스트도 pop되어 제거되면 이벤트 루프가 콜 스택이 비어있는 것을 확인하여 태스크 큐의 콜백함수를 콜 스택으로 푸시한다.
브라우저가 호출하여 콜백함수의 실행 컨텍스트가 생성되고 콜백함수를 실행한 뒤 종료되면 실행 컨텍스트 스택에서 pop되어 제거되고 코드가 종료된다.
7번에서 브라우저가 콜백함수를 호출한다는 의미는 태스크 큐에 있던 콜백함수를 콜 스택으로 이동시켜준다는 의미이다.
다음 시간에는 비동기 함수의 callback 패턴과 promise 패턴, async, await에 대해서 알아보자.