✏️ 인터랙티브 자바스크립트 시작하기
1-01. 웹 서비스와 JavaScript
- JavaScript는 웹 페이지의 여러 요소들을 좀 더 자유롭게 다루기 위해 등장
- HTML : 웹 페이지 구성
- CSS : 웹 페이지 디자인
- JavaScript : 웹 페이지 동작 -> 어떤 버튼을 클릭하면 웹 페이지의 디자인이 바뀐다거나.. 그런 동작
1-02. HTML/CSS + JavaScript 맛보기
- HTML과 CSS의 한계 : 한 번 설계한 페이지에 특별히 변화를 주는 것이 어려움
- 일반적으로 <body> 맨 밑에 <script> 추가
<body>
<script src = "index.js></script>
</body>
- JS를 이용해 실시간으로 HTML에 변화를 주거나 스타일을 수정할 수 있음
1-03. id로 태그 선택하기
- 존재하지 않는 태그라면 null값이 return
// id로 HTML 태그 선택
const myTag = document.getElementById('ID_name'); //ID 태그 안에 내용을 전부 포함하게 됨
1-05. class로 태그 선택하기
- id를 이용하면 여러 원소를 동시에 선택하기는 어려움 -> class 사용
- Elements인 것에 주의 !
//class로 태그 선택하기
const myTags = document.getElementsByClassName('class_name'); //선택한 class에 속하는 태그들이 배열 형태처럼 return
- return값이 사실 완벽한 배열은 아니다 -> 배열 메소드는 사용할 수 없기 때문
- But, 대괄호표기법으로 인덱스에 접근, length, for of문 사용 가능
- 이런 특성을 가진 애들을 유사배열(Array-Like Object)라고 부른다
- 인덱스는 HTML 코드 작성된 차례대로 정해짐 -> 깊이와는 상관 no
- 원소가 존재하지 않는 class를 선택하면 -> 빈 HTMLCollection이 출력
1-06. 유사 배열(Array-Like Object)이란?
- 유사 배열 조건
- 숫자 형태의 indexing 가능
- length 프로퍼티 -> index가 있어도 length값이 존재하지 않는다면 그냥 숫자 형태의 key로 구성된 일반 객체
- 배열의 기본 메소드 사용 불가능
- Array.isArray('유사배열') 값이 false
- 유사 배열이 이미 만들어진 경우라면 위의 4가지 조건을 만족할 것.
1-08. 태그 이름으로 태그 선택하기
const btns = document.getElementsByTagName('button');
- HTML 내에 모든 'button' 태그를 선택
- 여러 요소를 선택할 수 있기 때문에 HTMLCollection으로 결과값 return
- 인자를 '*'로 주면 모든 태그 선택 가능
- 너무 범위가 커서 의도치 않은 실수를 할 수 있기 때문에 자주 사용되지는 않음
1-09. css 선택자로 태그 선택하기
// css 선택자로 태그 선택하기
const myTag = document.querySelector('#ID_NAME'); //class 선택할 때는 '.class_name'
- class 이름을 넣게 되면 HTMLCollection 형태로 return되는 게 아니라 가장 첫번째 요소만 선택됨
- 여러 요소 선택하려 querySelectorAll() 사용 -> NodeList라는 유사 배열이 return값 => 요소가 없을 때 null값 return!!
- getElement~ 메소드보다 querySelector 메소드가 활용도가 더 높다!
1-11. 이벤트와 버튼 클릭
- 이벤트 : 웹 페이지에서 일어날 수 있는 대부분의 일 (ex. 마우스 움직이기, 마우스 클릭, 페이지 스크롤 등)
- 이벤트 핸들링 : 자바스크립트를 통해 이벤트를 다루는 일
- 이벤트 핸들러 : 이벤트가 발생했을 때 동작하는 코드
// 이벤트와 버튼 클릭
const btn = document.querySelector('#myBtn');
// 이벤트 핸들링(Event Handling)
btn.onclick = function (){ // 이벤트 핸들러(리스너)
console.log('function call!');
}
- 이벤트 핸들링 코드를 HTML 파일에 직접 작성할 수도 있지만 권장 x
1-12. [실습] 정답일까?
- 버튼을 클릭하면 경고창이 뜨는 코드
const btn = document.querySelector('#grade');
btn.onclick = function(){
alert('정답입니다!👏🏻');
}
✏️ 브라우저와 자바스크립트
2-01. 브라우저도 객체다?
- window : 브라우저 자체를 가리키는 객체 -> 자바스크립트에서 최상단에 존재하는 객체(자바스크립트의 모든 객체를 포함하고 있다는 뜻)
- 원래라면 window.console.log() 이런 식이지만 모든 객체가 window에 속하기 때문에 안붙여도 됨
// window : 전역객체 (Global Object)
console.log(window);
console.log(window.innerWidth);
console.log(window.innerHeight);
2-02. DOM
- DOM(Document Object Model) : 문서 객체 모델 -> 웹 페이지에 나타나는 HTML 문서 전체를 객체로 표현한 것
- document 객체를 사용하면 -> 웹 페이지 내부의 무엇이든 수정 / 추가 가능
console.log(document); // HTML 문서 자체가 출력(태그 형태)
console.dir(document); // 객체 형태로 다양한 프로퍼티들이 출력
2-03. console.dir?
- console.log()와 console.dir() 모두 입력받은 값을 콘솔에 출력하는 역할
- dir 메소드는 무조건 문자열로 출력
- log는 값 자체를, dir은 객체의 속성을 출력 ⭐
- log는 파라미터로 여러 개 가능, dir은 하나만 가능
- log는 DOM 객체를 HTML 형태로 출력, dir은 객체 형태로 출력 ⭐⭐⭐
2-04. DOM 트리
[ 위치 관계 ]
- 부모/자식 노드 : 상하 관계에 있는 노드들
- 형제 노드(Sibling node) : 같은 위치 상에 있는 노드들
[ 내용 ]
- 요소 노드(Element node) : 태그를 담고 있는 노드 (html, head, body 등)
- 텍스트 노드(Text node) : 문자열을 표현하는 노드 (그림 상에서 회색 노드) -> 자식 노드를 가질 수 없기 때문에 leaf node
- 일반적으로 텍스트 노드는 요소 노드의 자식 노드
- 이 밖에도 코멘트 노드, 문서 노드 등이 있지만 잘 사용되지는 않는다.
2-05. DOM 트리 여행하기
- DOM 트리의 관계 이용해서 요소 선택하기
const myTag = document.querySelector('#content');
//자식 요소 노드
console.log(myTag.children); //myTag의 모든 child 요소들이 HTMLCollection으로 출력됨
console.log(myTag.childtren[1]);
console.log(myTag.firstElementChild);
console.log(myTag.lastElementChild);
//부모 요소 노드
console.log(myTag.parentElement);
//형제 요소 노드
console.log(myTag.previousElementSibling);
console.log(myTag.nextElementSibling);
2-06. DOM 트리 여행하기 : 부록
- 요소 노드 이외에 모든 노드에 적용되는 프로퍼티
node.childNodes //node의 자식 노드 모음(NodeList)
node.firstChild //node의 첫 번째 자식 노드
node.lastChild //node의 마지막 자식 노드
node.parentNode // node의 부모 노드 하나
node.previousSibling //node의 prev/left에 있는 노드 하나
node.nextSibling //node의 next/right에 있는 노드 하나
2-08. 요소 노드 프로퍼티
//요소 노드 주요 프로퍼티
const myTag = document.querySelector('#list-1');
//innerHTML : 요소 안에 HTML을 수정할 때 활용
console.log(myTag.innerHTML); //요소 안에 있는 HTML 자체를 문자열로 return
myTag.innerHTML += '<li>new</li>';
//outerHTML : 요소 자체를 가리키기 때문에 요소 자체가 수정됨
console.log(myTag.outerHTML); //요소를 포함한 HTML 자체를 문자열로 return
myTag.innerHTML = '<li>new</li>'; //이런 식으로 하면 #list-1 요소가 아예 바껴버리는 것 ,,
//textContent : 특수문자도 전부 텍스트로 처리하는 것 주의!
console.log(myTag.textContent); //요소 안에 있는 내용 중 HTML 제외한 텍스트만 return
myTag.textContent = '<li>new text!</li>'; //이렇게 하면 태그로 인식하는 게 아니라 모두 텍스트로 인식함
2-11. 요소 노드 추가하기
- 요소 노드를 직접 생성해서 추가하는 방법
const tomorrow = document.querySelector('#tomorrow');
//1. 요소 노드 만들기 : document.createElement('태그이름');
const first = document.createElement('li');
//2. 요소 노드 꾸미기 : textContent, innerHTML ...
first.textContent = '처음';
//3. 요소 노드 추가하기 : NODE.prepend, append, after, before
tommorow.prepend(first); //tommorow 요소 가장 첫번째 자식 요소로 추가
tommorow.append(first); //tommorow 요소 가장 마지막 자식 요소로 추가
tommorow.before('문자열', first); //tommorow 요소 이전 요소(앞쪽)로 추가
tommorow.after(first); //tommorow 요소 다음 요소(뒤쪽)로 추가
2-12. 노드 삭제와 이동하기
//노드 삭제하기 : Node.remove();
tomorrow.remove(); //tomorrow 노드가 삭제됨
today.children[2].remove();
//노드 이동하기 : prepend, append, before, after
today.append(tomorrow.children[1]); //today의 맨 앞으로 tomorrow의 자식 요소 1번을 이동
tomorrow.children[1].after(today.children[1]);
2-14. HTML 속성 다루기
- 속성 접근하기
//elem.getAttribute('속성'): 속성에 접근하기 -> 비표준 속성에도 접근 가능
console.log(tomorrow.getAttribute('href'));
//여기서부터는 표준 속성에만 접근 가능
//id 속성
console.log(tomorrow.id);
//class 속성
console.log(item.className);
//href 속성
console.log(link.href);
- 속성 추가하기
// elem.setAttribute('속성', '값'): 속성 추가(수정)하기
tomorrow.setAttribute('class', 'list'); //추가
link.setAttribute('href', 'naver.com'); //수정
- 속성 삭제하기
//elem.removeAttribute('속성') : 속성 제거하기
tomorrow.removeAttribute('class');
2-16. 스타일 다루기
//style 프로퍼티
today.children[0].style.textDecoration = 'line-through';
- style 프로퍼티를 이용하면 값이 HTML 코드에 직접 style 속성으로 추가됨 -> 오류 발생할 가능성 있음!
- 미리 class를 만들어두고, 그 class를 추가하는 식으로 스타일 적용시킴
//elem.className -> 아예 클래스 자체를 바꿔버린다!
today.children[1].className = 'done'; //미리 작성해둔 done 클래스의 css 스타일이 적용됨
//elem.classList: add, remove, togggle
tomorrow.children[1].classList.add('done', 'other'); //기존 클래스에 done, other 클래스 추가
tomorrow.children[1].classList.remove('done'); //done 클래스 삭제
tomorrow.children[1].classList.toggle('done'); //없으면 추가, 있으면 삭제
tomorrow.children[1].classList.toggle('done', true); //true면 추가만, false면 삭제만! -> 자주사용NO
2-18. 비표준 속성 다루기
[status] { /* status 속성을 가진 태그들 선택 */
padding: 5px 10px;
}
[status="대기중"] { /* status 속성값이 '대기중'인 태그들을 선택 */
background-color: #FF6767;
color: #FFFFFF;
}
- 비표준 속성 활용하기
- 선택자로 활용
const fields = document.querySelectorAll('[field]');
- 값을 표시할 태그를 구분할 때 활용
- 스타일이나 데이터 변경에 활용
- 비표준 속성을 사용하는 것은 안전하지 않을 수 있다 -> 그 속성이 나중에 표준으로 등록될 수도 있기 때문!
=> data-'비표준 속성'으로 값을 설정해주고 속성값을 가져올때는 element.dataset.비표준속성 으로 접근!
✏️ 이벤트 살펴보기
3-01. 이벤트 핸들러 등록하기
- 하나의 요소에 여러 개의 독립적인 이벤트를 추가할 때
function event1() {
console.log('Hi Codeit!');
}
function event2() {
console.log('Hi again!');
}
// elem.addEventListener(event, handler) : 이벤트 핸들러 등록
btn.addEventListener('click', event1);
btn.addEventListener('click', event2);
// elem.removeEventListener(event, handler) : addEventListner로 등록한 이벤트 핸들러 삭제
btn.removeEventListener('click', event2); //반드시 이벤트 등록할 때 쓴 함수의 이름으로!!
3-02. 다양한 이벤트 ⭐⭐⭐
- 마우스 이벤트
- mousedown : 마우스 버튼을 누르는 순간
- mouseup : 마우스 버튼 눌렀다 떼는 순간
- click : 마우스 왼쪽 버튼 클릭
- dbclick : 마우스 왼쪽 버튼 더블 클릭
- contextmenu : 마우스 오른쪽 버튼 클릭
- mousemove : 마우스를 움직이는 순간
- mouseover : 마우스 포인터가 요소 위로 올라왔을 때
- mouseout : 요소에서 벗어났을 때
- mouseenter : 마우스 포인터가 요소 위로 올라왔을 때 (버블링 x)
- mouseleave : 요소에서 벗어났을 때 (버블링x)
- 키보드 이벤트
- keydown : 키보드 버튼 누르는 순간
- keypress : 출력가능한 키보드 버튼 (ex. shift, esc 등에서는 작동 x)
- keyup : 버튼 눌렀다 떼는 순간
- 포커스 이벤트
- focusin : 요소에 포커스 되는 순간
- focusout : 요소로부터 포커스 빠져나간 순간
- focus : 요소에 포커스 되는 순간 (버블링 x)
- blur : 요소로부터 포커스 빠져나간 순간 (버블링 x)
- 입력 이벤트
- change : 입력된 값이 바뀔 때
- input : 값이 입력될 때
- select : 입력 양식의 하나가 선택될 때
- submit : 폼을 전송할 때
- 스크롤 이벤트
- scroll : 스크롤 바 움직일 때
- 윈도우 창 이벤트
- resize : 윈도우 사이즈를 움직일 때 발생
3-04. 이벤트 객체
// 이벤트 객체 (Event Object)
const myInput = document.querySelector('#myInput');
const myBtn = document.querySelector('#myBtn');
function printEvent(event) { //이벤트 핸들러의 첫번째 파라미터는 무조건 이벤트 객체가 전달!
console.log(event);
event.target.style.color = 'red';
}
myInput.addEventListener('keydown', printEvent);
myBtn.addEventListener('click', printEvent);
3-05. 이벤트 객체 프로퍼티
- 공통 프로퍼티
- type : 발생한 이벤트의 type (이벤트 이름)
- target : 이벤트가 발생한 해당 dom 요소
- currentTarget : 이벤트 핸들러가 등록된 요소
- timeStamp : 이벤트 발생 시각 (페이지 로드된 시각부터 밀리초로 계산된 값)
- bubbles : 버블링 단계인지 판단하는 값
- 마우스 이벤트
- 키보드 이벤트
3-06. [실습] 완료한 일 체크하기!
- JS
const toDoList = document.querySelector('#to-do-list');
const items = toDoList.children;
// 1. updateToDo 함수를 완성해 주세요
function updateToDo(event) {
event.target.classList.toggle('done');
}
// 2. 반복문을 활용해서 각 li태그에 이벤트 핸들러를 등록해 주세요
for (item of items){
item.addEventListener('click', updateToDo);
}
// 테스트 코드
items[2].removeEventListener('click', updateToDo);
3-07. 이벤트 버블링
- 이벤트 버블링 : 부모 요소의 이벤트 핸들러가 같이 동작하는 것(윈도우 객체까지 모두 타고 올라가며 작동된다)
- 버블링된다고 이벤트 target까지 부모 요소로 변하는 것은 아님! -> target은 처음 이벤트가 발생한 시작점 그대로
- currentTarget : 실제로 이벤트가 발생중인 target (자주 사용x)
- 버블링 막는 법 -> 권장 x, 버블링에 대해 잘 알고 설계하면 버블링 막을 일이 별로 없을 것
for (let item of items) {
item.addEventListener('click', function(e) {
console.log('item Event');
console.log(e.currentTarget);
e.stopPropagation(); //버블링을 막는다
});
}
3-08. 캡쳐링
- 표준 DOM 이벤트에서 정의한 이벤트 흐름
- 캡쳐링 단계 : 이벤트가 하위 요소로 전파되는 단계
- 타깃 단계 : 이벤트가 실제 타깃 요소에 전달되는 관계 -> 가장 처음 이벤트 핸들러가 동작하는 순간
- 버블링 단계 : 이벤트가 상위 요소로 전파되는 단계
- 캡처링 단계에서 이벤트 핸들러 동작시키는 법 : addEventListener()의 세번째 프로퍼티에 true
3-10. 이벤트 위임
- 각 아이템마다 이벤트 핸들러를 달아주면 새로운 아이템을 추가할 때마다 새로 등록해 줘야한다는 문제가 있음
- 이벤트 위임 : 자식 요소에서 발생하는 이벤트를 부모 요소에서 한번에 관리하 방식 -> 버블링 이용
// 이벤트 위임 (Event Delegation)
const list = document.querySelector('#list');
list.addEventListener('click', function(e) {
// if (e.target.tagName === 'LI')
if (e.target.classList.contains('item')) { //item이 해당 요소의 class 속성에 있는지 확인
e.target.classList.toggle('done');
}
});
const li = document.createElement('li');
li.classList.add('item');
li.textContent = '일기 쓰기';
list.append(li);
- 자식 요소에는 아무런 이벤트가 등록되지 않았으니 이벤트 발생 x -> 부모 요소의 이벤트 실행 (이런 원리를 이용한 것)
3-12. 브라우저의 기본 동작
// 브라우저의 기본 동작
const link = document.querySelector('#link');
const checkbox = document.querySelector('#checkbox');
const input = document.querySelector('#input');
const text = document.querySelector('#text');
//event.preventDefault : 브라우저 기본 동작 막기
link.addEventListener('click', function(e) {
e.preventDefault();
alert('지금은 이동할 수 없습니다.');
});
input.addEventListener('keydown', function(e) {
if (!checkbox.checked) {
e.preventDefault();
alert('체크박스를 먼저 체크해 주세요.');
}
});
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
alert('마우스 오른쪽 클릭은 사용할 수 없습니다.');
});
✏️ 다양한 이벤트 알아보기
4-01. 마우스 버튼 이벤트
- 더블 클릭 이벤트 주의할 점 : 클릭 이벤트가 2번 일어난 후에 더블클릭 이벤트가 발생
/**
* [마우스 버튼 이벤트]
*
* > MouseEvent.button
* 0: 마우스 왼쪽 버튼
* 1: 마우스 휠
* 2: 마우스 오른쪽 버튼
*
* > MouseEvent.type
* click: 마우스 왼쪽 버튼을 눌렀을 때
* contextmenu: 마우스 오른쪽 버튼을 눌렀을 때
* dblclick: 동일한 위치에서 빠르게 두번 click할 때
* mousedown: 마우스 버튼을 누른 순간
* mouseup: 마우스 버튼을 눌렀다 뗀 순간
*/
4-03. 마우스 이동 이벤트 I
/**
* [마우스 이동 이벤트]
*
* > MouseEvent.type
* mousemove: 마우스 포인터가 이동할 때
* mouseover: 마우스 포인터가 요소 밖에서 안으로 이동할 때
* mouseout: 마우스 포인터가 요소 안에서 밖으로 이동할 때
*
* > MouseEvent.clientX, clientY
* : 화면에 표시되는 >창 기준< 마우스 포인터 위치
*
* > MouseEvent.pageX, pageY
* : 웹 문서 >전체 기준< 마우스 포인터 위치
*
* > MouseEvent.offsetX, offsetY
* : 이벤트가 발생한 >요소 기준< 마우스 포인터 위치
*/
const box1 = document.querySelector('#box1');
function onMouseMove(e) {
console.log(`client: (${e.clientX}, ${e.clientY})`);
console.log(`page: (${e.pageX}, ${e.pageY})`);
console.log(`offset: (${e.offsetX}, ${e.offsetY})`);
console.log('------------------------------------');
}
box1.addEventListener('mousemove', onMouseMove);
4-05. 마우스 이동 이벤트 II
/**
* [마우스 이동 이벤트]
*
* > MouseEvent.type
* mousemove: 마우스 포인터가 움직일 때
* mouseover: 마우스 포인터가 요소 밖에서 안으로 움직일 때
* mouseout: 마우스 포인터가 요소 안에서 밖으로 움직일 때
*
* > MouseEvent.target
* : 이벤트가 발생한 요소
*
* > MouseEvent.relatedTarget
* : 이벤트가 발생하기 직전(또는 직후)에 마우스가 위치해 있던 요소
*/
const box2 = document.querySelector('#box2');
function printEventData(e) {
console.log('event:', e.type);
console.log('target:', e.target);
console.log('relatedTarget:', e.relatedTarget);
console.log('------------------------------------');
if (e.target.classList.contains('cell')) {
e.target.classList.toggle('on');
}
}
box2.addEventListener('mouseover', printEventData);
box2.addEventListener('mouseout', printEventData);
- mouseover : relatedTarget -> target (마우스 들어옴)
- mouseout : target -> relatedTarget (마우스 나감)
- 2개의 이벤트는 항상 동시에 발생한다.
4-06. mouseenter / mouseleave
- 버블링이 일어나지 않는다. (mouseover/mouseout은 일어남)
- 자식 요소의 영역을 계산하지 않는다.
- 이벤트가 자식 요소에 영향을 끼치는지가 가장 큰 차이!
4-08. 키보드 이벤트
/**
* [키보드 이벤트]
*
* > KeyboardEvent.type
* keydown: 키보드 버튼을 누른 순간
* keypress: 키보드 버튼을 누른 순간
* keyup: 키보드 버튼을 눌렀다 뗀 순간
*
* > KeyboardEvent.key
* : 이벤트가 발생한 버튼의 값
*
* > KeyboardEvent.code
* : 이벤트가 발생한 버튼의 키보드에서 물리적인 위치
*/
- keypress 이벤트는 하나의 키를 계속 누르고 있어도 1번만 발생하지만, keydown은 계속 이벤트 발생
- keypress 이벤트는 출력이 가능한 키를 누를 때만 발생 -> 출력이 가능하더라도 영어가 아닐 때는 반응 x
- keypress는 별로 권장 x
4-09. [실습] 똑Talk한 Enter키!
const chatBox = document.querySelector('#chat-box');
const input = document.querySelector('#input');
const send = document.querySelector('#send');
function sendMyText() {
const newMessage = input.value;
if (newMessage) {
const div = document.createElement('div');
div.classList.add('bubble', 'my-bubble');
div.innerText = newMessage;
chatBox.append(div);
} else {
alert('메시지를 입력하세요...');
}
input.value = '';
}
send.addEventListener('click', sendMyText);
// 여기에 코드를 작성하세요
function checkEnter(event){
if(event.key == 'Enter' && !event.shiftKey){
event.preventDefault(); //기본 동작(줄바꿈) 금지 -> 초기화
sendMyText();
}
}
input.addEventListener('keypress', checkEnter);
4-10. input 태그 다루기
- change : 입력이 끝났다는 암시를 줘야 이벤트 발생 -> 엔터키를 누르거나 focusout이 되면 같이 발생!
/**
* [input 태그 다루기]
*
* > 포커스 이벤트
* focusin: 요소에 포커스가 되었을 때
* focusout: 요소에 포커스가 빠져나갈 때
* focus: 요소에 포커스가 되었을 때 (버블링 x)
* blur: 요소에 포커스가 빠져나갈 때 (버블링 x)
*
* > 입력 이벤트
* input: 사용자가 입력을 할 때
* change: 요소의 값이 변했을 때
*/
const el = document.querySelector('#form');
function printEventType(e) {
console.log('type:', e.type);
console.log('target:', e.target);
console.log('---------');
}
el.addEventListener('focusin', printEventType);
el.addEventListener('focusout', printEventType);
el.addEventListener('input', printEventType);
el.addEventListener('change', printEventType);
4-12. 스크롤 이벤트
// Scroll 이벤트
let lastScrollY = 0;
function onSroll() {
const nav = document.querySelector('#nav');
const toTop = document.querySelector('#to-top');
const STANDARD = 30;
if (window.scrollY > STANDARD) { // 스크롤이 30px을 넘었을 때
nav.classList.add('shadow');
toTop.classList.add('show');
} else { // 스크롤이 30px을 넘지 않을 때
nav.classList.remove('shadow');
toTop.classList.remove('show');
}
if (window.scrollY > lastScrollY) { // 스크롤 방향이 아랫쪽 일 때
nav.classList.add('lift-up');
} else { // 스크롤 방향이 윗쪽 일 때
nav.classList.remove('lift-up');
}
lastScrollY = window.scrollY;
}
window.addEventListener('scroll', onSroll);
23/09/26
이벤트는 진짜 해도해도 넘 어렵다 ,, 어떻게 해야 익숙해질 수 있는거지ㅠ