본문 바로가기
CoreJavaScript-study/presentation

[발표2회차] this와 콜백함수

by 어느새벽 2024. 7. 8.

this란

this는  자기가 속한 객체를 가리킨다. 객체를 속상과 메서드(함수)를 가질 수 있는 일종의 상자라고 생각해보면 this는 그 상자 안에서 "내가 지금 속한 상자가 무엇인지"를 알려주는 역할을 한다. 

아래 설명을 통해 각 상황에 따른 this를 살펴보자.

 

객체 내부에서 

const 사람 = {
    이름: '철수',
    자기소개() {
        console.log(this.이름); // this는 '사람' 객체를 가리킴
    }
};

사람.자기소개(); // "철수" 출력

 

this는 '사람'이라는 객체를 가리키고 있다. 그래서 'this.이름'은 '사람.이름'과 같다. 

 

전역에서는 this는 윈도우를 가리킨다.

전역에서 this는 브라우저 환경에서는 window라는 큰 상자를 가리킨다. 이 상자는 브라우저가 가지고 있는 모든 기능과 데이터를 포함하고 있다. 브라우저 환경에서의 전역 객체는 window이고, Node.js 환경에서의 전역 객체는 global이다.

console.log(this); // window 객체 출력

var a = 1;
console.log(a); // 1
console.log(window.a); // 1
console.log(this.a); // 1

 

위에 값들이 다 1로 나오는 이유는 자바스크립트의 모든 변수가 특정 객체의 프로퍼티로서 동작하기 때문이다. 특정 객체는 바로 실행 컨텍스트의 LexicalEnvironment(이하 L.E)이다. 실행 컨텍스트는 변수를 수집해서 L.E의 프로퍼티로 저장한다. 이후 어떤 변수를 호출하면 L.E를 조회해서 일치하는 프로퍼티가 있을 경우 그 값을 반환한다.

a를 직접 호출해도 1이 나오는 까닭은 변수 a에 접근하고자 하면 스코프 체인에서  a를 검색하다가 가장 마지막에 도달하는 전역 스코프의 L.E, 즉 전역객체에서 해당 프로퍼티 a를 발견해서 그 값을 반환하기 때문이다. 단순하게는 (window.)이 생략된 것이라고 생각하면 된다.

 

그런데 '삭제' 명령을 쓰게 되면 전역 객체의 프로퍼티로 할당한 경우에는 삭제가 되지만 전역 변수로 선언한 경우에는 삭제가 되지 않는다. 이는 의도치 않은 에러를 막기 위한 것으로 전역변수를 선언하면 자바스크립트 엔진이 자동으로 전역객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 false로 정의하기 때문이다.

함수 안에서의 this

함수 안에서 this는 조금 복잡해질 수 있다. 기본적으로 함수 안에서 this는 전역 객체를 가리킨다. 그런데 엄격한 규칙을 적용하면 this는 'undefined'가 될 수도 있다.

function 보여줘() {
    console.log(this); // 브라우저에서는 `window` 객체 출력
}

보여줘();

 

이 함수는 전역에서 호출되었기 때문에 this는 'window'를 가리킨다.

메서드 내부함수에서의 this의 경우 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지에 따라 판단할 수 있다. 우회하는 방법으로는 변수를 활용하는 것이다.

this를 바인딩하지 않는 함수의 경우 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자 this를 바인딩하지 않는 화살표함수가 새로 도입된다. 상위 스코프의 this를 그대로 활용할 수 있게된다. 그 밖에 call, apply 등의 메서드를 활용하는 법도 있다.

 

  • call  메서드
    call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 이때 call 메서드의 첫번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 한다. 함수를 그냥 실행하면 this는 전역 객체를 참조하지만 call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있다.
  • apply 메서드 
    apply메서드는 call메서드와 기능적으로 완전히 동일하다. call메서드는 첫번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정하는 반면, apply 메서드는 두번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 차이가 있다.
  • bind 메서드
    bind메서드는 call과 비슷하지만 즉시 호출하지는 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드이다. 다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤를 이어서 등록된다. 즉 bind 메서드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닌다. 
  • name 프로퍼티
    bind메서드를 적용해서 새로 만든 함수는 한가지 독특한 성질이 있는데 바로 name 프로퍼티에 동사 bind의 수동태인 bound라는 접두어가 붙는다는 점이다. bind메서드를 적용한 새로운 함수라는 의미이다.

 

메서드 내부에서의 'this'

메서드는 객체 안에 있는 함수이다. 이 메서드 안에서 this는 메서드가 속한 객체를 가리킨다.

const 고양이 = {
    이름: '야옹이',
    울기() {
        console.log(this.이름); // this는 '고양이' 객체를 가리킴
    }
};

고양이.울기(); // "야옹이" 출력

 

여기서 this는 고양이 객체를 가리킨다. 함수로서의 호출과 메서드로서의 호출은 함수 앞에 점(.)이 있는지 여부에 따라 다르다. 점이 없으면 함수로서 호출한 것이고, 점이 있으면 메서드로서 호출한 것이다. 대괄호 표기법의 경우에도 메서드로서 호출한 것이다.

생성자 함수에서의 this

생성자 함수는 새로운 객체를 만들 때 사용된다. new 키워드로 생성자 함수를 호출하면,  this는 새로 만들어진 객체를 가리킨다.

function 동물(이름) {
    this.이름 = 이름;
}

const 고양이 = new 동물('야옹이');
console.log(고양이.이름); // "야옹이" 출력

 

this는 새로 생성된 '고양이' 객체를 가리킨다.

 

화살표 함수에서의 this

화살표 함수는 조금 다르다. 이 함수는 외부의 this를 그대로 사용한다. 즉, 화살표 함수 내부에서는 this가 어디에서 불렸는지가 아니라, 함수가 정의된 위치에 따라 this가 결정된다.

const 사람 = {
    이름: '철수',
    자기소개: () => {
        console.log(this.이름); // 여기서 `this`는 외부의 `this`를 참조, 전역 객체를 가리킴
    }
};

사람.자기소개(); // undefined 출력

 

화살표 함수 내부의 this는 '사람'객체가 아니라 전역 객체를 가리키기 때문에 'this.이름'은 'undefined'가 된다.

 

*화살표 함수의 예외사항

화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐다. 즉 이 함수 내부에는 this가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 된다.

 

이벤트 핸들러에서의 this

이벤트 핸들러는 어떤 일이 일어났을 때(ex. 버튼 클릭) 호출되는 함수이다. 여기서 this는 그 이벤트를 일으킨 요소를 가리킨다.

const 버튼 = document.querySelector('button');
버튼.addEventListener('click', function() {
    console.log(this); // 클릭된 버튼 요소 출력
});

 

버튼을 클릭하면 this는 그 버튼 자체를 가리킨다.

 

별도의 인자로 this를 받는 경우(콜백함수 내에서의 this)

콜백함수를 인자로 받는 메서드 중 일부는 this로 지정할 객체(thisArg)를 인자로 추가하여 지정할 수 있는 경우가 있다. 이러한 메서드의 thisArg 값을 지정하면 콜백 함수 내부에서의 this 값을 원하는대로 변경할 수 있다. 이런 형태는 여러 내부 요소에 대한 같은 동작을 반복 수행해야 하는 배열 메서드에 많이 포진돼있다.


콜백함수란?

콜백함수는 다른 코드의 인자로 넘겨주는 함수로 직역하면 "되돌아 호출해달라"라는 뜻이다.

특정한 일이 끝난 후에 호출 되도록 미리 준비해 놓은 함수라고 생각하면 된다.

 

기본적인 콜백 함수 예제

function greeting(name) {
    console.log('Hello, ' + name + '!');
}

function processUserInput(callback) {
    const name = prompt('Please enter your name.');
    callback(name);
}

processUserInput(greeting);

 

위 코드에서는 두 개의 함수가 있다.

  1. greeting(name) 함수 : 이름을 받아서 인사말을 출력한다.
  2. processUserInput(callback) 함수 : 사용자에게 이름을 입력 받은 후, 그 이름을 콜백 함수로 전달한다.

processUserInput( greeting )을 호출하면, processUserInput 함수가 사용자에게 이름을 입력받고, 그 이름을 greeting  함수로 전달해서 인사말을 출력한다. 여기서 greeting  함수가 콜백 함수로 사용된 것이다.

 

비동기 콜백 예제

자바스크립트에서 콜백 함수는 특히 비동기 작업에서 많이 사용된다. 비동기 작업은 시간이 걸리는 작업을 처리하면서도 그 작업이 끝날 때까지 기다리지 않고 다른 작업을 계속 할 수 있게 해준다. 예를 들어 서버에서 데이터를 가져오는 작업이 있다.

console.log('Start');

setTimeout(function() {
    console.log('This runs after 2 seconds');
}, 2000);

console.log('End');

//result 
//Start
//End
//This runs after 2 seconds

 

위 코드에서는 setTimeout 함수를 사용해서 2초 후에 실행될 콜백 함수를 지정했다.

 

콜백 지옥

콜백 함수가 중첩되면 코드가 복잡해질 수 있다. 이를 '콜백지옥'이라고 부른다. 예를 들어, 여러 비동기 작업이 순차적으로 실행되어야 하는  경우가 있다.

doSomething(function(result1) {
    doSomethingElse(result1, function(result2) {
        doAnotherThing(result2, function(result3) {
            doSomethingMore(result3, function(result4) {
                console.log('All done!');
            });
        });
    });
});

 

이렇게 콜백 함수가 중첩죄면 가독성이 떨어지고 유지보수가 어려워진다.

 

콜백 함수의 대안

콜백 지옥을 피하기 위해 모두 변수로 선언할 수도 있겠지만 더 효율적인 방법이 있다. 자바스크립트에서는 'Promise'와 'async/await'를 사용해 비동기 작업을 더 간단하고 읽기 쉽게 처리한다.

Promise란? 

Promise는 자바스크립트에서 비동기 작업을 좀 더 효율적으로 처리하기 위한 객체이다. 비동기 작업은 예를 들어 네트워크 요청을 보내거나 파일을 읽는 등 시간이 걸리는 작업을 말한다. 이러한 작업은 일반적으로 동기적인 방식으로 처리하면 다른 코드의 실행을 막게 되어 사용자 경험을 저하시킬 수 있다. 이를 해결하기 위해 나왔다.

 

1. 비동기 작업 처리

Promise는 비동기 작업을 효율적으로 처리하기 위해 설계되었다. 예를들어, 네트워크 요청이나 파일 로딩과 같은 작업을 Promise를 사용하여 관리할 수 있다. 

2. 상태(State)

Promise 객체는 아래 중에 하나의 상태를 가진다.

  • 대기(Pending) : 초기 상태로, 작업이 완료되지 않은 상태이다.
  • 이행(Fulfilled) : 작업이 성공적으로 완료된 상태이다.
  • 거부(Rejected) : 작업이 실패한 상태이다.

3. 성공과 실패 처리 : Promise 객체는 작업의 성공 또는 실패에 따라 처리할 수 있는 콜백 함수들을 등록할 수 있다. 성공 시에는 resolve 콜백을 호출하여 처리 결과를 전달하고, 실패 시에는 reject 콜백을 호출하여 에러 정보를 전달한다.

4. 체이닝(Chaining) : 여러 개의 비동기 작업을 순차적으로 처리할 때 유용하게 사용할 수 있다. Promise 는 .then()과 .catch() 메서드를 통해 다른 Promise나 값을 반환하는 방식으로 체이닝될 수 있다.

// 비동기 작업을 Promise로 처리하는 함수 예제
function asyncOperation() {
  return new Promise((resolve, reject) => {
    // 비동기 작업 시뮬레이션 (예: setTimeout을 사용한 비동기 작업)
    setTimeout(() => {
      const success = true; // 성공 여부를 가정
      if (success) {
        resolve('비동기 작업 완료'); // 성공 시 resolve 호출
      } else {
        reject(new Error('작업 실패')); // 실패 시 reject 호출
      }
    }, 2000); // 2초 후에 작업 완료
  });
}

// Promise를 사용한 비동기 작업 실행 예제
asyncOperation()
  .then(result => {
    console.log('결과:', result); // 비동기 작업이 성공했을 때 실행할 코드
  })
  .catch(error => {
    console.error('오류 발생:', error); // 비동기 작업이 실패했을 때 실행할 코드
  });

 

위 예제에서 asyncOperation 함수가 Promise 객체를 반환하고 있다. 비동기 작업이 완료되면 resolve 또는 reject를 호출하여 그 결과 반환하거나 에러를 반환한다. 그리고 .then()과 .catch()를 사용하여 각각 성공과 실패 시의 처리를 정의한다.

 

async / await 함수

 

async 함수

async function fetchData() {
  // 비동기 작업을 수행하는 코드
}

 

이 함수는 항상 Promise를 반환한다. async 함수 내에서는 await 키워드를 사용하여 다른 Promise를 기다릴 수 있다.

 

await 키워드

await 키워드는 async 함수 내에서만 사용할 수 있다. 이 키워드를 만나면 JavaScript는 그 줄에서 Promise가 처리될 때까지 대기하고, 그 결과 값을 반환한다. 이 과정에서 코드 실행이 일시 정지된다.

async function fetchData() {
  let response = await fetch('https://api.example.com/data');
  let data = await response.json();
  return data;
}

 

위 예제에서 'fetchData' 함수는 'fetch'함수를 사용하여 데이터를 가져오고, 'awiat'를 통해 서버로부터의 응답이 올 때까지 기다린다. 그리고 응답을 JSON으로 파싱한 데이터를 반환한다.

 

예외처리

async function fetchData() {
  try {
    let response = await fetch('https://api.example.com/data');
    let data = await response.json();
    return data;
  } catch (error) {
    console.error('Error fetching data:', error);
    throw error; // 예외를 다시 던질 수도 있음
  }
}

 

async 함수 호출

async 함수는 Promise를 반환하므로, then/catch 구문을 사용하여 호출할 수 있다.

fetchData()
  .then(data => {
    console.log('Data:', data);
  })
  .catch(error => {
    console.error('Error:', error);
  });

 

또는 async 함수를 직접 호출하고, 그 안에서 await를 사용하여 결과를 처리할 수도 있다.

async function processData() {
  try {
    let data = await fetchData();
    console.log('Processed Data:', data);
  } catch (error) {
    console.error('Error processing data:', error);
  }
}

processData();

 

async/await를 사용하면 비동기 코드를 작성할 때 then 체이닝을 피하고, 보다 선언적이고 구조적인 방식으로 코드를 작성할 수 있다.

 

반응형