JS 이벤트 버블링
이벤트 핸들러가 실행될 때, 부모가 가지는 동일한 종류의 이벤트 핸들러까지 실행되는 현상이다. window 객체를 만나기 전까지 거품처럼 쭉 올라간다고 해서 '버블링'이라고 한다. 자식(하위) 요소에서 발생한 이벤트 정보를 부모(상위) 요소까지 전달하기 때문에 별도로 설정하지 않아도 항상 발생한다.
예를 들어 <ol id='list'>
과 그 자식 요소인 <li class='item'>
에 각각 아래와 같은 이벤트 핸들러를 넣었다고 가정하자.
const list = document.querySelector('#list');
const item = list.querySelector('.item');
list.addEventListener('click', function(e) {
console.log('list');
//console.log(e.target); // 이벤트가 발생한 위치(요소) 표시
});
item.addEventListener('click', function(e) {
console.log('item');
//console.log(e.target);
});
이 코드를 실행하고 item
을 클릭하면 콘솔에는 'item', 'list'가 출력된다. 즉 item
의 이벤트 함수가 실행되고 이어 부모인 list
의 함수까지 실행되는 것이다. 버블링을 멈추려면 e.stopPropagation();
을 쓰면 되지만 이렇게 하는 경우는 잘 없다.
그리고 한 가지 중요한 점은 e.target
으로 이벤트가 발생한 요소를 추적하면 부모의 이벤트라 하더라도 부모 요소가 아닌 자식 요소가 출력된다는 점이다. 즉 위 코드에서 두 이벤트 모두 <li>
요소를 타겟으로 반환한다. 이렇게 나오는 이유는 버블링이 하위 요소의 정보를 상위로 전달하기 위함임을 생각해보면 납득이 간다.
만약 이벤트가 발생한 요소가 아니라 함수가 실행된 그 요소를 보고 싶으면
e.currentTarget
을 이용하면 된다.
JS 이벤트 위임
이벤트 버블링을 이용해서 한 번의 자식 요소의 이벤트를 핸들링하는 것을 이벤트 위임이라고 한다. 이 블로그에 설명이 잘 되어 있다.
앞서 본 것처럼 자식 요소 item
을 클릭하면 해당 요소의 이벤트 함수에 이어 부모 요소 list
의 이벤트 함수까지 실행된다. 이걸 이용하면 여러 개의 item
자식 요소마다 이벤트 함수를 넣어줄 필요가 없다.
모든 class = item
요소를 클릭할 때마다 특정 함수를 실행시키려면 일일이 .addEventListener
을 할 필요 없이 아래와 같이 작성하면 된다.
const list = document.querySelector('#list');
const item = list.querySelector('.item');
list.addEventListener('click', function(e) {
console.log('My Function for all items');
});
이렇게 하면 여러 개의 자식 요소 item
중 무엇을 클릭하더라도 자동으로 그 부모 요소인 list
로 버블링이 될 것이고, 그것은 function(e)
를 실행시킬 것이다. 이렇게 이벤트를 부모 요소에 대신 넣어주는 방식을 이벤트 위임이라고 하고, 이벤트 버블링을 이용하는 것이니만큼 e.stopPropagation();
으로 버블링을 막으면 이벤트 위임은 동작하지 않는다.
이렇게 하면 자식뿐만 아니라 부모를 클릭해도 실행되잖아?
자식 요소가 아닌 부모 요소를 눌렀을 때 핸들러가 실행되는 것을 방지하려면 정확히 자식 요소를 눌렀는지 확인하는 단계를 추가하면 좋다. 예를 들어 이벤트가 발생한 타겟의 class 리스트에 `.item'이 있을 때에만 부모 이벤트 핸들러가 실행되게 만들면 된다.list.addEventListener('click', function(e) { if (e.target.classList.contains('item')){ console.log('My Function for all items'); } });
Node.js REPL(대화형 창)
터미널에서 node를 실행하여 사용자가 입력하는 JS를 실행하는 모드를 말한다. 낯설게 느껴지지만 터미널에서 파이썬을 실행하여 사용자가 대화형으로 코드를 입력하고 실행하는 것과 같은 것이다.
Read Evaluate Print Loop 의 약자로 사용자가 입력한 내용을 '읽고', 결과값을 '계산하고', 결과값을 '출력하고', 이 과정을 '반복한다'는 의미를 가지고 있다.
강의 듣고 만들면 강의 내용을 따라서 치는 수준이라서 퀴즈부터 풀어보기로!
JS 청기백기 게임 만들기
조건
1. 마우스 좌/우를 누르면 청/백기에up
클래스가 추가된다.
2. 마우스 우클릭 시 브라우즈 메뉴가 나오지 않는다.
첫 번째 답안:
const flagBlue = document.querySelector('.flag-blue');
const flagWhite = document.querySelector('.flag-white');
function reset() {
document.querySelector('.up').classList.remove('up');
}
// 1. 좌/우 클릭시 클래스 추가
function flagUp(e) {
if (e.button === 0){
flagBlue.classList.add('up');
}
else if (e.button === 2){
flagWhite.classList.add('up');
}
// 500 밀리초 뒤에 reset함수를 실행(깃발을 내림)
setTimeout(reset, 500);
}
// 2. 우클릭 방지
document.addEventListener('contextmenu', function (event) {
event.preventDefault();
});
// 테스트
// 'mousedown'은 눌렀다 뗄 때 실행되는 'click'과 달리 누른 순간 실행된다.
document.addEventListener('mousedown', flagUp);
찾아보니 witch
클릭 구분을 더 쉽게 할 수 있나보다. witch
다시 써보자.
function flagUp(e) {
switch (e.button){
case 0:
flagBlue.classList.add('up');
break;
case 2:
flagWhite.classList.add('up');
break;
}
setTimeout(reset, 500);
}
오호 이렇게 해도 된다!
switch
문법을 살펴보니 switch (expr)
의 expr
이 case X
의 X
와 정확히 일치하는지(===
) 확인한 후 실행된다고 한다. 뭔가 익숙하길래 파이썬에서 사용해봤나? 싶었는데 파이썬에는 switch
가 따로 없다고 한다(대신 match
가 업뎃 되었다고 함). 그 이유까지 찾아보려고 했는데 설명이 이해가 잘 안 가서 그냥 받아들이기로.
JS 키보드 이벤트
keydown
vs. keypress
둘 다 키보드를 누르는 순간 발생하는 이벤트이지만 keydown
은 모든 키보드를 인식하는 반면 keypress
는 기능 키(Shift, Ctrl 등)와 한글을 인식하지 않는다. 그리고 하나의 키를 계속 누르고 있으면 keydown
은 계속해서 발생하지만 keypress
는 딱 한 번 인식된다. 현재는 keypress
사용을 권고하지 않는다.
.key
vs. .code
키보드 이벤트의 key
프로퍼티는 키보드의 값 그 자체를 출력하는 반면 code
는 키보드의 물리적인 위치를 출력한다. 예를 들어 'A'를 누르면 key: A
, code: keyA
를, 우측 Shift를 누르면 key: Shift
, code: RightShift
를 출력한다.
JS 채팅 앱 만들기
'send' 버튼을 클릭하면 메시지가 전송되는 함수 sendMyTexT
는 이미 완성되어 있다. 여기에 Enter을 눌러 메시지를 전송하는 기능을 추가하자.
조건
1. Enter을 눌러 메시지를 전송하고textarea
태그는 초기화 한다.
2. Shift+Enter을 눌러 줄바꿈한다.
3.keypress
타입으로 핸들러를 등록한다.
중간 풀이: 아니... keypress
로 이벤트 등록하면 e.shiftKey
, e.key
모두 안 나오잖아? 이걸로 어떻게 Shift+Enter을 감지하라는거지? Enter로 보내는 함수야 간단한데 2번이 이해가 안 가네.
function keyboardSender(e){
if (e.key === 'Enter'){
e.preventDefault(); // Enter로 인한 줄바꿈 방지
sendMyText();
}
}
input.addEventListener('keypress', keyboardSender);
해설: 이상하게 console.log(e.shiftKey)
해두고 Shift를 눌렀을 때에는 아무 것도 안 나오더니, 막상 해설대로 따라하니 된다. 그리고 Shift+Enter가 줄바꿈이라는 건 직접 설정하지 않아도 된다는 점을 몰라서 더 헤맸다.
내가 시도한 방법:
function keyboardSender(e){
console.log(e.key);
console.log(e.shiftKey); // 아무것도 출력하지 않는다.
if (e.key === 'Enter'){
e.preventDefault(); // Enter로 인한 줄바꿈 방지
sendMyText();
}
else if (e.shiftKey && e.key === 'Enter'){
alert('test'); // 이 함수는 작동하지 않는다. 정확히 같은 타이밍에 누르는 게 불가능해서 그런가?
}
}
해설:
function keyboardSender(e){
// .shiftKey가 누르지 않은 상태라면 Enter로 인식한다는 방식으로 만든 함수.
// 근데 console.log(e.shiftKey)에는 아무것도 출력되지 않는데 이건 좀 이상하다.
if (e.key === 'Enter' && !e.shiftKey){
e.preventDefault();
sendMyText();
}
}
JS
<input>
이벤트
1) 포커스
<input>
태그를 클릭하면 (별도의 CSS를 적용하지 않는 한) 해당 입력칸이 파랗게 변하여 이용자에게 입력을 받을 준비가 되었다는 표시를 한다. 이 순간 focusin
이, 태그를 빠져나오면 focusout
이벤트가 발생한다. focus
와 blur
라는 이벤트도 있는데 이들은 버블링을 하지 않는다.
2) 입력
<input>
태그에 값이 입력되면 input
이벤트가 발생한다.keydown
과 유사해 보이지만 입력과 무관한 키(Shift 등) 입력은 이벤트를 발생시키지 않는다. 그리고 focusout
이벤트가 발생하거나 Enter을 누를 때 <input>
태그의 값이 이전과 달라진 경우 change
이벤트가 발생한다.
3) 요약
태그에 입력을 할 때 이벤트의 발생 순서는 아래와 같다. focusin
→input
→change
(값이 바뀌었을 때)→focusout
타자연습 만들기
게임을 실행할 때마다 임의의 단어가 <span>
태그로 생성되고, 각 단어는 data-word
라는 속성 값으로 해당 단어 텍스트를 가지고 있다. <input>
에 단어 텍스트를 입력하면 해당 단어가 사라지게 만들어야 한다.
조건
1. 입력값과 일치하는 단어를 가진 요소가 있다면 그것을 삭제한다.
2. 이벤트 핸들러가 호출되면<input>
태그는 초기화되어야 한다.
2. 단어를 삭제한 후에는 남은 단어의 개수를 세는checker
함수를 실행해야 한다.
중간 풀이:
입력값이 들어와야 실행되어야 하니까 이벤트는 change
로 잡고, <input>
태그의 값은 e.target.value
로 잡으면 된다.
function removeText(e) {
const words = document.querySelectorAll('.word');
const inputText = e.target.value;
e.target.value = ''; // 태그를 초기화 한다.
}
그러면 이 값이 words
노드 리스트의 특정 태그의 속성과 일치하는지 확인해야 하는데... 단어 아무거나 잡아서 오브젝트를 확인해보면 .dataset.word
이렇게 나온다. 이걸 어떻게 words
리스트 안의 word
를 하나하나 꺼내보지?
다른 코드 보니까 for (let word...)
코드가 있던데 따라해보니 아래 코드가 잘 작동한다.
function removeText(e) {
const words = document.querySelectorAll('.word');
const inputText = e.target.value;
for (let word of words) {
if (inputText === word.dataset.word) {
console.log("Gotcha!");
}
}
e.target.value = ''; // 태그를 초기화 한다.
}
이제 단어를 삭제하고 checker
함수를 추가하면 최종 코드는 아래와 같다.
답안:
function removeText(e) {
const words = document.querySelectorAll('.word');
const inputText = e.target.value;
for (let word of words) {
if (inputText === word.dataset.word) { // 단어의 오브젝트가 입력값과 동일한지 확인한다.
word.remove() // 해당 단어를 삭제한다.
}
}
e.target.value = ''; // 태그를 초기화 한다.
checker(); // checker 함수를 실행한다.
}
input.addEventListener('change', removeText)
모범 답안:
function removeWord() {
const word = document.querySelector(`[data-word="${input.value}"]`);
if (word) {
word.remove();
checker();
}
input.value = '';
}
input.addEventListener('change', removeWord);
나와 다른 점
e.target.value
대신 간단하게 input.value
로 입력값을 찾았다.words
를 불러오고 그 안의 word
를 하나하나 for문으로 돌리며 확인했지만, 이 코드는 querySelector
로 애초에 프로퍼티가 입력값을 가지는 단어만 추출했다.null.remove()
가 발생할 가능성이 없지만, 이 코드는 querySelector
로 아무것도 안 나올 경우를 대비해서 if(word)
문을 추가했다.querySelector
로 처음부터 맞는 단어만 골라낸다는 발상은 하지 못했다. 매번 for문 돌리는 내 코드보다 훨씬 간결하다.
JS
11-12에 했던 채팅 앱 만들기에서 의문점에 대한 답을 받았다.
keypress
는 shift를 감지하지 않아서 shift를 눌러도 e.shiftKey
가 나오지 않는데, 어떻게 if (e.key === 'Enter' && !e.shiftKey)
가 작동할 수 있나?keypress
와 그로 인한e.shiftKey
가 모두 발생하지 않지만, shift+Enter을 누르면 Enter로 인해 keypress
가 작동하면서 e.shiftKey
값이 발생할 수 있다. 그래서 아래 코드로 Enter와 Shift+Enter을 차례로 누르면 다음과 같이 나온다.function keyboardSender(e){
console.log("e.key: " + e.key);
console.log("e.shiftKey: " + e.shiftKey);
}
그래서 shift+Enter을 누를 때 if (e.key === 'Enter' && !e.shiftKey)
가 false가 되어 메시지가 보내지지 않고 Shift를 안 누른 채 Enter을 누르면 메시지가 보내지는 것이다.
JS 플젝
이제 기본 동작은 알았으니 웹에 필요한 fetch, API 등도 알아야 하는데... 이걸 계속 강의로 듣고 있자니 좀이 쑤셔서 ㅠㅠ 다른 플젝을 생각했다. 내가 제일 자주 쓸 것 같은 #TODO 를 JS로 만들어야지(카카오톡 챗봇은 API까지 배워야해서 예상보다 늦게 만들 것 같다).
일단 구상은 이렇게! (맘같아선 음악 듣게 유튜브도 넣고 싶은데 우선 이 정도로) TODO 체크하면 완료 리스트로 넘어가고, 넘어간 건 한 번 더 누르면 삭제되도록!
앗 플젝 좋은 거 생각났다!!! 근무기록 쓸 때 필요한 기록지 만들어야지!! (둘 다 있음 좋고) 근데 어째 구상하다보니 좀... 복잡해보인다?
오늘은 맨 위 근무시각 입력 칸까지 만들었다! 깃허브에 올리는 중~
플젝 (하다가 알게 된 점)
.createElement
로 만든 <input>
에 required
속성을 넣는 방법; newinput.required = True;
이런 방법으로 넣을 수 있는 속성을 reflected property 라고 부른다. 음... 설명이 아직은 이해가 잘 가지 않는다. 참고로 이게 불가능한 속성은 element.setAttribute("required", "");
와 같이 속성을 준다.
submit
이벤트는 창을 새로고침하기 때문에 e.preventDefault();
를 해줘야 한다.
오늘 만든 것! Start를 누르면 기본 블록(시작/끝 시간, 텍스트 입력 칸)이 나오게 만들었다. 내일은 Start를 hidden으로 만들고 끝 시간 등등을 설정해야겠다.
플젝
addBlock
함수를 그 블록 내의 버튼에서 다시 불러내려고 했다. 처음에는 <input type="submit">
을 사용했는데 분명 addBlock
안에 e.preventDefault()
가 있음에도 불구하고 자꾸 새로고침이 돼서 왜지...하고 고민하던 차에 <button>
태그로 바꾸었더니 기대했던 대로 동작한다. 왜 <input>
만 동작방지가 안 먹히는지 이유를 모르겠다... 검색해도 두 태그의 기능 자체는 동일하다고 나오는데 흠 🤔오늘은 새 블록을 추가하는 '입력'버튼을 만들고 첫 번째 블록의 시작 시간은 Start와 동일하게 나오도록 설정했다! 이제 그 다음 블럭의 시작 시간은 이전 블럭의 종료 시간을 따르게 만들면 된다. 이전 블럭의 종료 시간은 반드시 채워지도록 설정하고!
플젝
const startTime = document.createElement("input");
startTime.type = "time";
startTime.required = true;
startTime.step = "900";
const endTime = startTime;
그런데 이렇게 하니까 startTime
, endTime
요소가 각각 생기는 게 아니라 그냥 startTime
하나만 나오더라! endTime = startTime;
이게 값을 복사하는 게 아니라 주소를 복사하는 건가보다. (왜 항상 헷갈리는지...) 노드를 복제하려면 startTime.cloneNode()
를 쓰면 된다.
이제 종료시간은 시작시간에 +30분 되게 설정했고, 다음 블록의 시작시간은 직전 종료시간을 이어오도록 만들었다! 블록 편집이 좀 더 자유로우면 좋을텐데 좀 고민해봐야겠다 ㅎㅎㅎ
플젝
21일; '휴식' 체크박스를 체크하면 '휴식'이 입력되고 해제하면 텍스트가 삭제되게 만들었다.
22일; 블록이 일정 길이를 넘어가면 스크롤이 되도록 CSS를 추가했다. 지금까지 입력한 모든 블록의 텍스트를 추출하는 함수를 만들었다. 이제 시계를 추출하고, 집계를 내는 함수를 추가하면 가장 중요한 기능은 마무리된다!
JS
fetch
함수fetch('https://www.google.com')
.then((response) => response.text())
.then((result) => { console.log(result); });
fetch(url)
url로 요청을 보내는 함수. 그 응답으로 '프로미스(Promise)' 객체가 돌아오면 .then
구문이 동작한다. 이렇게 특정 조건을 만족할 때까지 기다리는 함수를 '콜백'이라고 부른다. 콜백은 등록한 순서대로 실행되고 이전 콜백의 리턴값을 이후의 콜백이 넘겨받아 사용한다.
scheme
(http); 통신 규약(프로토콜)의 이름. HyperText Transfer Protocol의 약어로 HyperText를 주고받기 위한 프로토콜이라는 의미. 해당 프로토콜 규칙에 따라 서버와 클라가 요청-응답을 주고 받음.host
(www.naver.com); 하나의 서버를 특정하는 주소. 도메인은 좀 더 좁은 의미로 사람이 읽기 편한 형태로 바꾼 주소를 말함. (참고: Difference b/w Host name and Domain name)path
; 서버에서 원하는 데이터를 특정query
(?pg=1); 세부적인 요구사항 특정플젝
reduce
함수를 써야 한다(유사한 질문). reduce
는 map
처럼 배열을 돌면서 값을 누적해서 연산해주는 함수인데 여기를 먼저 읽고 공식문서를 읽는 게 이해하기 좋다. [{text: "업무1", time: "1:30"}, {text: "업무2", time: "1:00"}...]
이런 식으로 배열을 만든 다음에 예시에 나온 대로 분류한 후 time을 합치는 식으로 진행해야겠다.map
을 사용하면 된다. arr1.map((a, i) => a + arr2[i]);
map
등의 함수를 사용할 수 없다. array 함수를 쓰려면 다음과 같이 변환해주어야 한다.const myArr = Array.prototype.slice.call(myNodeList);
JS JSON
JavaScript Object Notation의 약자로 JS 객체 문법(key-value)을 빌린 데이터 형식을 말한다. 다만 JS와 문법이 완전히 동일하지는 않다. 아래는 두 문법의 차이점이다.
플젝
startTime과 endTime의 시간 차이를 계산하려면 map
함수를 써야한다.
startTime = ["9:30", "10:00"]
, endTime = ["10:00", "11:00"]
이렇게 나올 때 해야 할 일은 1. 각각을 split해서 [[9, 30], [10, 00]]
이런 형식으로 만들고 2. [n][0]
, [n][1]
을 각각 시와 분으로 두어서 Date를 생성한 다음 3. 두 Date 간의 차이를 "0:30"
형식으로 출력해서 빈 array에 push해야 한다.
뭔가 map 함수가 파이썬이랑 비슷한 것 같기도 한데 좀 낯설다... 두 개의 array를 동시에 쓸 때는 let zip = (a1, a2) => a1.map((x,i) => [x, a2[i]]);
이렇게 index i
로 다른 array를 가져오는 것 같은데 좀만 더 찾아봐야겠다.
음... 콘솔에서 하려니까 불편한데 ipython 같은 건 없나... 일단 테스트한 걸 하나의 함수로 바꿔봐야겠다.
A = ["9:00", "10:00"];
B = ["10:00", "11:00"];
let C = A.map((x,i) => [x.split(":"), B[i].split(":")]);
>> [Array(2), Array(2)]
>> ['9', '30'] ['10', '00']
>> ['10', '00'] ['11', '00']
let D = C.map((x,i) => [C[0][i][0]-C[1][i][0]]);
>> [Array(1), Array(1)]
>> [-1]
>> [-1]
JS
JSON.parse
JSON.parse(myJson)
; string 타입의 myJson
을 JS 객체로 만들어주는 함수.
메소드
일반적으로 서버에 보내는 요청은 조회, 추가, 수정, 삭제 네 종류로 각각 요청(리퀘스트)의 GET, POST, PUT, DELETE 메소드로 전달된다.
리퀘스트는 헤드와 바디로 구분되는데 어떤 요청을 보낼지 결정하는 메소드는 헤드에, 실제로 담아야 하는 데이터는 바디에 전달된다(데이터를 받을 필요가 없는 GET, DELETE는 바디에 데이터가 따로 들어가지는 않는다).
사진 출처: 코드잇
플젝 map
vs. forEach
둘 다 array의 요소를 순차로 도는(?) 메소드인데 아래와 같은 차이가 있다(출처: 블로그1, 블로그2).
forEach
return something
을 해도 항상 undifined가 나온다. 콜백 함수로 이미 존재하는 array의 값을 바꿀 수 있다. iterating 하는 배열 자체를 수정할 수 있으므로 mutator 메소드이다. (모르는 단어가 또...)let arr = [1, 2, 3, 4, 5];
// arr 배열의 값에 2씩 곱하기
arr.forEach((num, index) => {
return arr[index] = num * 2;
});
// arr을 출력하면 [2, 4, 6, 8, 10]
map
map
을 수행하는 것도 가능하다.let numberArray = [1, 2, 3, 4, 5];
// 숫자 배열을 [$1, $2...]와 같이 달러 텍스트 배열로 변환하기
let returnValue = numberArray
.map((num) => num * 2)
.map((num) => num.toString())
.map((string) => "$"+string);
map
은 콜백 함수의 결과를 새로운 배열로 반환하고, forEach
는 그렇지 않으므로 용도에 맞게 사용하면 된다.