addEventListener 콜백함수 제대로 이해하기

Ywoosang·2021년 1월 31일
17

DOM

목록 보기
1/1
post-thumbnail

들어가며

오랜만에 프론트엔드 코드를 만져보니, 별게 다 헷갈린다.가끔씩 참조할 수 있도록 콜백함수에 관련된 부분을, 그 중에서도 이벤트 리스너 부분을 집중적으로 정리해본다.

콜백 함수 개념

MDN 정의에 따르면 콜백 함수는 다른 함수에 인자로 전달되는 함수이며, 외부 함수 내에서 일종의 루틴 또는 동작을 실행하기 위해 호출된다.

아래는 동기적으로 실행되는 콜백함수의 예시다.

function one(value){
    console.log(value);

}; 
function two(callback){
    const value = 'callback test';
    callback(value); 
};
two(one);

two 함수에 one 함수를 인자로 전달해 two 함수 내부에서 값을 전달해 호출했다.

setTimeout 과 setInterval 메소드도 콜백함수를 필요로 한다.
콜백함수의 형태는 화살표 함수든 function 꼴이든 상관없다.

setTimeout(()=>{
    console.log('callback test');
},0);

setInterval(()=>{
    console.log('callback test')
}, 500);

addEventListener 에서 콜백 사용

이벤트 리스너에서 콜백을 사용할 때 필요한 개념을 정리했다. 클로저에 대한 부분은 간략하게 설명하고 넘어간다.

클로저

아래 설명에서 클로저의 개념을 사용한다. 간단하게 설명하자면 내부함수가 외부함수의 지역변수에 접근할 수 있는것이 클로저다. 보통 함수를 리턴하는 꼴로 자주 사용한다.

내부 함수가 외부함수의 지역변수를 참조하며, 참조의 개념이기 때문에 별도 내부 메소드에서 외부함수의 지역변수 값을 변경하면, 변경된 값을 참조하게 된다.

outter 함수가 반환한 함수를 inner 변수에 할당한다. inner 변수는 함수이므로 실행해보면 콘솔에 결과값으로 value 가 출력된다. 외부 함수인 outter 는 실행이 종료되어 사라졌지만, outter 의 지역변수인 value 값을 inner 함수에서 여전히 참조하고 있는 것을 볼 수 있다.

이러한 클로저의 개념으로 데이터은닉을 설명할 수 있지만, 별도의 포스팅으로 다루기로 하고 여기까지만 설명하겠다.

function outter(value){
    // 인자로 전달된 value 는 지역변수 역할을 한다. 따라서 외부함수 outter 의 지역변수로 볼 수 있다. 
    return function(){
        console.log(value);
    }
}
const inner = outter('value');  
// 외부함수의 지역변수인 value 에 접근 가능 
inner(); 

익명 함수를 콜백함수로 전달

기본적으로 이벤트 리스너를 아래와 같이 전역 스코프에 등록해서, 해당 이벤트가 target 요소에 전달될때마다 콜백함수를 실행 시킨다.

target.addEventListener('click',()=>{
    console.log('callback test');
});

물론 아래와 같이 function 키워드를 사용해도 상관없지만, 보통 화살표함수를 사용한다.

target.addEventListener('click',function(){
	console.log('callback test');
});

이벤트 객체 참조

e.target 과 같이 이벤트 객체를 참조하고 싶은 경우 아래를 사용한다.

참고
내부적으로 이벤트가 발생하면 등록한 콜백을 실행하되, 그 함수의 인자로 (미리 스펙이 정의된) 이벤트 객체를 만들어서 넣어준다. 콜백함수 첫 번째 인자로 넣어준 대상을 이벤트 객체로 참조하는데 보통 e 혹은 event 로 이름을 정한다.

target.addEventListener('click',(e)=>{
    console.log(e);
    console.log('callback test');
}); 

이벤트 객체를 콘솔에 출력해보면 아래와같이 스펙이 정의되어 있다.

별도로 콜백 함수를 분리

콜백함수를 분리해서 사용하고 싶은 경우도 있다. 아래와 같이 별도의 함수를 정의하고 콜백함수로 등록해 사용한다.

function callback(e){
    console.log(e);
    console.log('callback test');
}

target.addEventListener('click',callback);

함수 안에서 이벤트리스너 등록

몇몇 이유로 함수 안에서 이벤트리스너를 등록해야 할 경우가 있다. 간단한 예제로 각각 div 마다 자식요소로 button 을 추가하고 이벤트리스너를 등록해보자.

<button type="button">Button Appear</button>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>

클릭시 버튼들을 생성하기위해 Appear 버튼에 클릭 이벤트를 등록했다. 콜백함수로는 버튼을 생성하는 makeBtn 함수를 등록한다.

forEachdiv 태그가 담긴 배열을 순회하면서 각각의 div 태그 안에 이벤트리스너를 등록한 button 요소를 추가한다.

이렇게 각각 요소마다 이벤트리스너를 등록시키는 기능을 하는 콜백 함수를 만들고 이벤트리스너를 등록할 수 있다.

const divs =[ ...document.getElementsByTagName('div')] 
const appearBtn = document.getElementsByTagName('button')[0];
appearBtn.addEventListener('click',makeBtn)

function makeBtn(){ 
    divs.forEach(elem => {
        const btn = document.createElement('button');
        btn.textContent = 'Example' ;
        btn.addEventListener('click',(e)=>{
            console.log('callback test');
        })
        elem.appendChild(btn);
    });
}

콜백함수에 지역변수 전달

함수 안에서 이벤트리스너를 등록할 때, 함수의 지역변수를 인자로 전달해야 하는 경우가 있다.
아래처럼 데이터베이스에서 댓글을 불러와 화면에 띄워주고, 댓글 삭제와 수정버튼을 만드는 함수가 있다고 하자.

async function getComment(id) {
  try {
    const res = await axios.get(`/users/${id}/comments`);
    ... 
    const user_id = id
    comments.map(function (comment) {
      const comment_id = comment.id 
      ... 
      const edit = document.createElement('button');
      edit.addEventListener('click',() => editComment(user_id,comment_id)); 
      ... 
      const remove = document.createElement('button');
      remove.addEventListener('click',()=> removeComment(user_id,comment_id));
      ... 
    });
  } catch (err) {
    console.error(err);
  }
}

수정과 삭제를 담당하는 버튼을 만든다. 각각의 버튼마다 이벤트리스너를 등록해, 버튼 클릭 시 백엔드에 댓글을 수정, 삭제하는 요청을 보내는 콜백함수를 호출할 수 있다.

콜백함수에 지역변수를 전달하기 위해 익명함수를 만들고, 그 안에서 지역변수를 인자로 받은 함수를 실행시킨다.

이렇게 만들면 클릭 이벤트가 발생했을 때, 익명 화살표 함수가 실행되면서 익명함수 안에있는 코드도 같이 실행되기 때문에 원하는 결과를 얻을 수 있다.

const edit = document.createElement('button');
edit.addEventListener('click',() => editComment(user_id,comment_id)); 

const remove = document.createElement('button');
remove.addEventListener('click',()=> removeComment(user_id,comment_id));

호출하는 콜백함수는 아래와 같다. 데이터베이스에 댓글들을 수정,삭제하는 코드다.

async function editComment(user_id,comment_id) {  
  ... 
  try {
    await axios.patch(`/comments/${comment_id}`, { comment: newComment });
    ... 
  } catch (err) {
    console.error(err);
  }
}
// 댓글 삭제
async function removeComment(user_id,comment_id) {  
  try {
    await axios.delete(`/comments/${comment_id}`);
    ... 
  } catch (err) {
    console.error(err);
  }
}

별도로 함수를 분리해 인자를 전달하지 않고 함수 안에 등록한 이벤트리스너의 콜백함수만을 이용할 수 있다. 이렇게 되면 이벤트리스너의 콜백함수 안에 모든 내용이 들어가게 되는데, 코드가 길어지기때문에 별도로 분리한 것이다.

... 
edit.addEventListener('click', async() => {
  ... 
  try {
    await axios.patch(`/comments/${comment_id}`, { comment: newComment });
    ... 
  } catch (err) {
    console.error(err);
  }
}
} );  
... 

클로저 형태 사용

위 형태에서 명시적으로 클로저 형태를 사용할 수 있다. 함수 실행형태를 이벤트 리스너의 두 번째 인자로 넘기고, 실행하는 함수에서 내부함수를 반환하면 된다.

const edit = document.createElement('button');
edit.addEventListener('click',editComment(user_id,comment_id)); 

사실상, 이렇게 코드를 짜면 콜백함수를 전달한 것이나 다름없다. 자바스크립트가 코드를 읽을 때 editComment(user_id,comment_id) 를 실행하고 반환값인 async function(){..} 이 콜백함수로 등록된다.

function editComment(user_id,comment_id) { 
	return async function(){
  ... 
  try {
    await axios.patch(`/comments/${comment_id}`, { comment: newComment });
    ... 
  } catch (err) {
    console.error(err);
  }
}} 

이벤트 객체 사용 시 헷갈릴 수 있는 부분

이벤트객체 사용 시 헷갈릴 수 있는 부분을 정리했다.

지역변수와 이벤트객체를 함께 전달하는 방법

먼저, 콜백함수를 별도로 분리하고 익명의 함수를 만들어 지역변수를 전달하는 경우다.

아래 예제에서 이벤트 객체를 사용해야하는 상황이라고 가정하자.
그대로 실행해보면 아래와같이 e 변수에 이벤트 객체가 할당되지 않고, 순서대로 a,b 변수가 할당되어 마지막 값은 undefined 임을 확인할 수 있다.

콜백함수 내부에서 또다른 함수를 호출한 것이기 때문에 이벤트객체가 정의되지 않은 것이다.

function makeElem(){ 
    divs.forEach(elem => {
        const btn = document.createElement('button');
        btn.textContent = 'Example' ;
        let a = 'a'
        let b = 'b'
        btn.addEventListener('click', ()=> callback(a,b));
        elem.appendChild(btn);
    });
};

function callback(e,a,b) {
    console.log(e,a,b)
}

여기에 이벤트 객체를 전달하고 싶으면 콜백함수에 인자로 넘겨주면 된다.

function makeElem(){ 
    divs.forEach(elem => {
        const btn = document.createElement('button');
        btn.textContent = 'Example' ;
        let a = 'a'
        let b = 'b'
        btn.addEventListener('click', (e)=> callback(e,a,b));
        elem.appendChild(btn);
    });
};

function callback(e,a,b) {
    console.log(e,a,b)
}

이벤트 객체가 정상적으로 출력되는 것을 확인할 수 있다.

한 가지 주의할 것은, 이벤트 객체를 인자로 받고 또다시 다른 함수를 실행할 때 인자로 넘겨주는 형태의 방식이기 때문에 콜백함수에서 이벤트 객체를 인자로 받지 않는다면 오류가 난다.

btn.addEventListener('click', ()=> callback(e,a,b));

e 변수가 정의되지 않았기 때문이다. 이때 e 는 스펙에서 정의된 이벤트 객체가 아니라 이벤트 변수로 취급해야 한다. 함수를 실행할때 전달하는 인자기 때문이다.

callback(e,a,b); 

콜백함수에 이벤트 객체를 담은 함수 실행 형태

아래처럼 함수 실행 형태로 이벤트 객체를 전달할 수 없는 이유에 대해 질문한 글들을 많이 볼 수 있다.

btn.addEventListener('click',callback(e));

이벤트 객체를 전달할 목적이라면 불가능하다. 콜백함수는 실행 가능한 함수 형태로 넘겨주는 것이지, 실행시킨 형태를 전달하는것은 아니다.

위와같이 코드를 작성하게 되면, 클릭 이벤트가 발생했을 때 실행되는 것이 아니라, 자바스크립트가 코드를 한줄 한줄 읽으면서 즉시 실행해버린다.

다시말해, 클릭시 콜백함수로 동작하지 않고 애시당초에 콜백함수도 아니다. 함수를 실행시킨 형태기 때문이다.

따라서 별도의 함수로 분리해 이벤트 객체를 받으려면 아래처럼 사용하자.

btn.addEventListener('click',callback);

function callback(e){
	... 
} 

혹은 콜백함수 자체에서 코드를 작성할 수도 있다. 이벤트 객체를 전달받는 가장 일반적인 형태다.

btn.addEventListener('click',(e)=> {
	... 
});
profile
백엔드와 인프라에 관심이 많은 개발자 입니다

0개의 댓글