0%

드래그했을 때 mouseup 이벤트가 작동하지 않는 이유

요소를 잡아 끌고 놓았을 때는 해당 요소의 mouseup 이벤트가 발생하지 않기 때문이다.
즉, 잡아 끌 해당 요소에 mouseup 이벤트를 걸었기 때문이다.

해결 방법

mouseup 이벤트를 doucment에 걸어주는 것으로 해결할 수 있다.

잡아 끌 해당 요소에 mouseup 이벤트를 걸면 안되는 이유

마우스 왼쪽 클릭 시

  1. mousedown
  2. mouseup
  3. click 이벤트 순으로 발생한다.

마우스 오른쪽 클릭 시

  1. mousedown
  2. mouseup
  3. contextmenu 이벤트 순으로 발생한다.

위와 같이 요소 위에서 클릭이 이루어지는 경우엔 마우스 포인터가 움직이지 않으므로 mouseup이벤트를 해당 요소에 걸어도 문제가 없다.

하지만 마우스 포인터가 요소를 벗어난 상태에서는 해당 요소에 mouseup 이벤트가 발생할 수 없다. mouseup이벤트는 그 순간 마우스 포인터가 있는 곳에서 발생하기 때문.

마우스 왼쪽 클릭 후 잡아 끄는데 해당 요소가 draggable false면

  1. mousedown
    만 일어난다.
    이미 마우스 포인터가 요소를 벗어난 상태이므로 mouseup과 click은 발생하지 못한다.

마우스 왼쪽 클릭 후 잡아 끄는데 해당 요소가 draggable true면

  1. mousedown
  2. dragend 이벤트 순으로 발생한다.
    이미 마우스 포인터가 요소를 벗어난 상태이므로 mouseup과 click은 발생하지 못한다.

이벤트 전파

DOM 트리 상에 존재하는 DOM 요소 노드에서 발생한 이벤트가 DOM 트리를 통해 전파되는 것을 말한다.
이벤트 전파는 이벤트 객체가 전파되는 방향에 따라 3단계로 구분할 수 있다.1

  1. 캡쳐링 단계(capturing phase) : 이벤트가 상위 요소 -> 하위 요소 방향으로 전파
  2. 타깃 단계(target phase) : 이벤트가 이벤트 타깃에 도달
  3. 버블링 단계(bubbling phase) : 이벤트가 하위 요소 -> 상위 요소 방향으로 전파

그림 1

클릭 이벤트를 예시로 그림1을 보면, div를 클릭했기 때문에 클릭 이벤트의 시발점은 div가 된다.
이 때, body와 html 등 클릭 이벤트의 상위 요소에 이벤트 시발점의 이벤트와 동일한 이벤트가 존재한다면, 그 이벤트들도 탑승하게 된다.
이처럼 하나의 이벤트가 발생했을 때, 다른 이벤트들도 영향을 받아 실행될 수 있어 그 이벤트들의 순서에 대한 기준이 필요하다.
그 기준이 이벤트 플로우, 이벤트 전파이다.

이벤트 전파는 캡쳐링 단계 -> 타깃 단계 -> 버블링 단계 순으로 진행된다.

target vs currentTarget

target은 이벤트 전파를 발생시킨, 이벤트 시발점이다.
그림1에서 보면, 모든 이벤트 전파의 시발점이 div이므로 div객체의 이벤트에서도 div이고, body객체의 이벤트와 html객체의 이벤트에서도 div이다.
currentTarget은 어떤 이벤트가 실행되었을 때, 그 이벤트의 주인이다.
그럼1에서 보면, div객체의 이벤트는 div객체의 것이고, body객체의 이벤트는 body객체의 것, html객체의 이벤트는 html객체의 것이므로, 순서대로 div, body, html이 된다.

target과 currentTarget이 일치하는 경우, 이벤트 전파 단계 중 2. 타깃 단계(target phase)에서 실행된다.
target과 currentTarget이 일치하지 않는 경우, 1. 캡쳐링 단계(capturing phase)와 3. 버블링 단계(bubbling phase) 중 어느 단계에서 발생시킬 지 선택할 수 있다. (택1이다.)
선택하는 방법은 아래와 같이 addEventListener()를 사용하여 이벤트를 등록할 때, 필수가 아닌 옵션 매개변수인 useCapture를 이용하는 것이다.
useCapture에 인수를 할당하지 않으면, 기본값 false로 들어가 3. 버블링 단계(bubbling phase)에서 이벤트를 발생시키겠다고 지정하는 것이다.
true를 넣어주면 캡쳐링 단계(capturing phase)에서 이벤트를 발생시키겠다고 지정할 수 있다.
EventTarget.addEventListener(‘event Type’, functionName, [,useCapture])2

예시1

코드펜에서 보기

See the Pen Event Propagation Example by HyunJungC-Dev (@hyunjungc-dev) on CodePen.

위 코드를 실행하고 div를 클릭한다면 그림 2와 같이 이벤트 전파가 이루어진다.
그림 2
코드를 보면 div, body, html 모두 click 이벤트를 가지고 있어 div를 클릭했을 때 body와 html의 click 이벤트도 영향을 받게 된다.
addEventListener()를 보면 div, body, html 모두 useCapture에 인수가 할당되지 않으므로 기본값 false를 가진다.
따라서 target과 currentTarget이 일치하지 않는 body와 html의 경우 3. 버블링 단계(bubbling phase)에서 이벤트를 발생시키도록 코딩되어 있다.

div는 target과 currentTarget이 일치하므로, 2. 타깃 단계(target phase)에서 이벤트를 발생시킨다.

이벤트 전파는 항상 1. 캡쳐링 단계(capturing phase) -> 2. 타깃 단계(target phase) -> 3. 버블링 단계(bubbling phase) 순서대로 이루어진다.
위의 경우, 1. 캡쳐링 단계(capturing phase)에서 실행될 이벤트는 없다.
다음으로, 2. 타깃 단계(target phase)에서 div의 click 이벤트가 실행되어 Console에 ‘DIV 클릭 이벤트 발생’이라는 문자열을 찍게 된다.
마지막으로 3. 버블링 단계(bubbling phase)에서는 하위 요소->상위 요소 방향으로 진행되므로 body의 click 이벤트가 먼저 실행되어 Console에 ‘BODY 클릭 이벤트 발생’이라는 문자열을 찍는다. 그 다음 html의 click 이벤트가 실행되어 Console에 ‘HTML 클릭 이벤트 발생’이라는 문자열을 찍게 된다.

예시2

그럼 div가 아닌 body를 클릭하면 어떻게 될까?
그림 3과 같은 순서대로 이벤트 전파, 이벤트 실행이 일어나게 된다.
그림 3

예시3

코드펜에서 보기

See the Pen Event Propagation Example01 by HyunJungC-Dev (@hyunjungc-dev) on CodePen.

이제 1. 캡쳐링 단계(capturing phase)에 실행되는 이벤트가 있는 경우를 보자.
코드를 보면 body.addListener()의 useCapture에 true를 넘겨준다. 즉, body의 이벤트는 1. 캡쳐링 단계(capturing phase)에 실행되도록 설정했다.
그림 4
그러면 그림 4와 같이 1. 캡쳐링 단계(capturing phase)에서 body의 이벤트 객체가 실행되고, 2. 타깃 단계(target phase)에서 div의 이벤트 객체가 실행된다. 그리고 마지막으로 3. 버블링 단계(bubbling phase)에서 html의 이벤트 객체가 실행되어 Console에는 ‘BODY 클릭 이벤트 발생’, ‘DIV 클릭 이벤트 발생’, ‘HTML 클릭 이벤트 발생’ 순으로 찍히게 된다.

착각하지 말아야할 것

코드펜에서 보기

See the Pen Event Propagation Example03 by HyunJungC-Dev (@hyunjungc-dev) on CodePen.


1: 모던 자바스크립트 Deep Dive p779 40.6 이벤트 전파
2: MDN EventTarget.addEventListener()

정리 이유

백준 11723번 집합을 풀 때, 파이썬 입력함수에 따라 시간 초과가 나고 안 나고가 결정될 만큼 시간 차이가 있어 각 입력함수별 차이를 익히기 위해 정리해둔다.

input()

  • 문자열로 입력받는다.
  • list(map(int, input().split(‘ ‘)))을 이용해 공백으로 나누어 받을 수 있다.

sys.stdin.readline()

  • import sys 를 해주어야 한다.
  • 문자열로 입력받는다.
  • readline()은 문자열 마지막에 개행문자(줄바꿈 문자\n)을 포함하고 있음을 주의하자.
  • sys.stdin.readline()**.rstrip()**으로 오른쪽 공백을 삭제할 수 있다.
  • sys.stdin.readline()**.lstrip()**으로 왼쪽 공백을 삭제할 수 있다.
  • sys.stdin.readline()**.strip()**으로 오른쪽, 왼쪽 공백을 삭제할 수 있다.
  • (이 때 공백에는 개행문자, 스페이스 바 등이 해당된다.)
  • list(map(int, stdin.readline().split(‘ ‘)))을 이용해 공백으로 나누어 받을 수 있다.

sys.stdin.readline(인수)

  • 인수에는 한번에 읽어들일 문자의 수를 넣어준다.
  • 그럼 그 숫자만큼의 길이만 읽어들인다.
    1
    2
    3
    num = sys.stdin.readline(2)
    # 입력 : abcdef
    # print(num) 결과 : ab

결론

알고리즘 최적화를 위해서 input() 대신 sys.stdin.readline()을 사용하는 것이 좋다.

시간 초과가 난 이유

  1. 비트마스크를 이용해 풀지 않았기 때문
  2. 파이썬 입력함수 중 input()을 사용했기 때문

시간 초과 해결방법1

원래 이 문제는 비트마스크를 이용하는 문제이다.
비트 마스크를 이용하면 시간 초과가 나지 않는다.
비트 마스크를 사용한 풀이

TIL/python
https://hyunjungc-dev.github.io/2021/05/28/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EC%9E%85%EB%A0%A5%ED%95%A8%EC%88%98-%EB%B9%84%EA%B5%90/

이 문제의 경우, 비트 마스크를 사용하지 않은 풀이에서 파이썬의 입력함수에 따라 시간초과가 나기도하고 아니기도 해서 파이썬의 입력함수에 대해 TIL/python 에 정리했다. 원래는 비트 마스크를 이용하는 문제이니 그렇게 푸는 것이 맞다.

시간 초과가 난 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
m = int(input())
S = set()
for _ in range(m):
cmd = input()
if cmd == "all":
S = {i for i in range(1, 21)}

elif cmd == "empty":
S = set()
else:
cmd, x = cmd.split(' ')
x = int(x)
if cmd == "add":
S.add(x)
elif cmd == "check":
print(int(x in S))
elif cmd == "remove":
S.discard(x)
elif cmd == "toggle":
if x in S:
S.discard(x)
else:
S.add(x)

시간 초과 해결 방법 2

파이썬 입력함수 중 sys.stdin.readline()을 사용한다.
위 함수로 입력을 받을 경우 맨 끝에 개행문자(줄바꿈 문자 \n)을 포함하고 있음을 주의해야한다.

시간 초과를 해결한 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import sys

m = int(sys.stdin.readline())
S = set()
for _ in range(m):
cmd = sys.stdin.readline()

if cmd == "all\n":
S = {i for i in range(1, 21)}

elif cmd == "empty\n":
S = set()
else:
cmd, x = cmd.split(' ')
x = int(x)
if cmd == "add":
S.add(x)
elif cmd == "check":
print(int(x in S))
elif cmd == "remove":
S.discard(x)
elif cmd == "toggle":
if x in S:
S.discard(x)
else:
S.add(x)

정리 이유

문자열을 뒤집을 때 항상 list로 변환하여 list의 reverse()함수를 사용하였는데 보다 빠른 슬라이싱으로 가능한 방법이 있어서 기억하기 위해 정리해둔다.

슬라이싱을 사용하여 문자열 뒤집기

s[::-1]

처음 접근

트리의 지름 구하는 공식에 따라 처음은 root인 1 부터 가장 긴 노드 A를 찾고 그 노드 A에서 가장 멀리 있는 노드 B를 찾아, 노드 A와 노드 B 사이의 path 거리를 구했다.

RecursionError 발생 원인

가장 멀리 있는 노드를 찾을 때 dfs를 recursion으로 구현하였는데
조건을 보면 노드의 개수 n(1 ≤ n ≤ 10,000) 이므로
최악의 경우 recursion이 10000번 일어나게 된다.
파이썬에서 recursion 수는 최대 998번으로 제한하고 있으므로 maximum recurion에 의해 RecursionError가 발생한 것이다.

해결 방법

recursion으로 구현한 dfs()를 Iteration(stack)으로 구현해주면 된다.

RecurionError가 발생한 코드 - dfs()를 recursion으로 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import collections

n = int(input())

tree = [collections.defaultdict(int) for i in range(n+1)]
path = [0 for i in range(n+1)]
visited = [False for i in range(n+1)]
for _ in range(n-1):
p, c, w = map(int, input().split(' '))
tree[p][c] = w


def dfs(start, before_node, path_length):
if visited[start] is True:
return
visited[start] = True

if tree[start][before_node] != 0:
path_length += tree[start][before_node]

elif tree[before_node][start] != 0:
path_length += tree[before_node][start]

path[start] = path_length

for adj_v in tree[start]:
dfs(adj_v, start, path_length)


dfs(1, 1, 0)

for i in range(len(visited)):
visited[i] = False

max_path_length = 0
max_path_node = 0
for idx, p in enumerate(path):
if max_path_length < p:
max_path_length = p
max_path_node = idx

dfs(max_path_node, max_path_node, 0)


print(max(path))

Iteration(stack)으로 변경한 dfs()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def dfs(start, path_length):
stack.append((start, start, path_length))
while stack:
curr, before_node, pl = stack.pop()
if visited[curr] is False:
visited[curr] = True
if tree[curr][before_node] != 0:
pl += tree[curr][before_node]

elif tree[before_node][curr] != 0:
pl += tree[before_node][curr]

path[curr] = pl

for adj_v in tree[curr]:
stack.append((adj_v, curr, pl))

RecursionError를 해결한 코드 - dfs()를 Iteration(stack)으로 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import collections

n = int(input())

tree = [collections.defaultdict(int) for i in range(n+1)]
path = [0 for i in range(n+1)]
visited = [False for i in range(n+1)]
for _ in range(n-1):
p, c, w = map(int, input().split(' '))
tree[p][c] = w

stack = []


def dfs(start, path_length):
stack.append((start, start, path_length))
while stack:
curr, before_node, pl = stack.pop()
if visited[curr] is False:
visited[curr] = True
if tree[curr][before_node] != 0:
pl += tree[curr][before_node]

elif tree[before_node][curr] != 0:
pl += tree[before_node][curr]

path[curr] = pl

for adj_v in tree[curr]:
stack.append((adj_v, curr, pl))


dfs(1, 0)

for i in range(len(visited)):
visited[i] = False

max_path_length = 0
max_path_node = 0
for idx, p in enumerate(path):
if max_path_length < p:
max_path_length = p
max_path_node = idx

dfs(max_path_node, 0)

print(max(path))

정리 이유

백준 1967번 트리의 지름을 풀 때, 아무리 고민해도 시간 초과를 해결하지 못해 조금 검색해보니 트리의 지름을 구하는 공식이 있었기에 정리해둔다.

트리의 지름 구하는 공식

임의의 노드 A에서 가장 거리가 먼 노드 B를 구하고,
그 B에서 가장 거리가 먼 노드 C를 구하게 되었을때,
B와 C사이의 거리트리의 지름이 된다.

생각못했던 것1

계단 오르기와 동일한 문제라고 생각했는데 차이가 있었다.

계단 오르기는 마지막 계단을 반드시 밟아야한다는 조건이 있었지만
포도주시식은 마지막 잔을 반드시 마셔야한다는 조건이 없었다.
즉, 포도주 시식의 경우, 마지막 잔을 안 마시는게 더 포도주를 많이 마실 수 있다면 그 경우를 찾아줘야한다.

가령 문제의 예시를 보면, 6잔의 포도주에
6 10 13 9 8 1 이 있을 때
기존의 계단오르기 처럼 하면
6 10 13 9 8 1 을 마셔 32를 최대라고 하는
6 10 13 9 8 1 이 경우가 33으로 최대이다.

잘못 짰던 코드 - 계단 오르기와 동일한 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import collections

n = int(input())
wines = collections.defaultdict(int)
for i in range(1, n+1):
wines[i] = int(input())

dp = collections.defaultdict(int)

<br>

# dp[i] = i번째를 반드시 마신다는 가정하에, i번째까지의 최대 와인 양
dp[1] = wines[1]
dp[2] = wines[1]+wines[2]
dp[3] = max(wines[1]+wines[3], wines[2]+wines[3])

for i in range(4, n+1):
dp[i] = max(dp[i-2]+wines[i], dp[i-3]+wines[i-1]+wines[i])
print(dp[n])

위 원인을 반영한 코드 - 그래도 틀린 코드

dp[i] = max(dp[i-2]+wines[i],dp[i-3]+wines[i-1] + wines[i], dp[i-1])에서
dp[i-1]을 추가했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import collections

n = int(input())
wines = collections.defaultdict(int)
for i in range(1, n+1):
wines[i] = int(input())

dp = collections.defaultdict(int)
# dp[i] = i번째를 마시든 안 마시든, i번째까지의 최대 와인 양
dp[1] = wines[1]
dp[2] = wines[1]+wines[2]
dp[3] = max(wines[1]+wines[3], wines[2]+wines[3])


for i in range(4, n+1):
dp[i] = max(dp[i-2]+wines[i],
dp[i-3]+wines[i-1] + wines[i], dp[i-1])
print(dp[n])

생각못했던 것2

dp[3]도 dp[2]와 비교해서 더 큰 값을 넣어줘야 했다.

최총 맞은 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import collections

n = int(input())
wines = collections.defaultdict(int)
for i in range(1, n+1):
wines[i] = int(input())

dp = collections.defaultdict(int)
# dp[i] = i번째를 마시든 안 마시든, i번째까지의 최대 와인 양
dp[1] = wines[1]
dp[2] = wines[1]+wines[2]
dp[3] = max(wines[1]+wines[3], wines[2]+wines[3], dp[2])


for i in range(4, n+1):
dp[i] = max(dp[i-2]+wines[i],
dp[i-3]+wines[i-1] + wines[i])
dp[i] = max(dp[i], dp[i-1])
print(dp[n])