코딩테스트 연습 - 미로 탈출 명령어

문제요약

n x m 격자 미로

(x, y) → (r, c)로 이동하여 탈출

  • 격자 밖으로 나갈 수 없음
  • (x, y) → (r, c) 이동거리는 총 k
  • (x, y), (r, c) 포함하여 같은 격자 2번 이상 방문 가능
  • 탈출경로를 문자열로 나타넀을 때, 사전 순으로 가장 빠른 경로 반환 (d : 아래 1칸, l: 왼쪽 1칸, r: 오른쪽 1칸, u: 위로 1칸)
  • 미로탈출 경로를 반환하되, 탈출 못하면 ‘impossible’반환

입력

2 ≤ n ≤ 50

2 ≤ m ≤ 50

1 ≤ x ≤ n

1 ≤ y ≤ m

(x, y) ≠ (r, c)

1≤ k ≤ 5^2*10^2

출력

return str

어떻게 풀까?

  • 시간복잡도 : 최대 50x50 크기의 격자이고 한 칸 당 최대 4방향으로 이동이 가능하며, k의 최대 값도 5^2*10^2인데다, 여러 경로가 나올 수 있고 그 중에서 사전 순으로 가장 빠른 경로를 반환해야함
    ⇒ 단순 BFS로 모든 경우를 산출한 뒤 그 중에서 사전 순으로 가장 빠른 답 반환하면 시간초과 발생
  • 사전 순으로 가장 빠르다
    ⇒ 매번 결정마다 가장 빠른 경로를 큐의 가장 앞에 넣어서 최단경로를 도출 필요
    ⇒ 다익스트라 활용 가능
  • 다익스트라를 활용하더라도, 이 문제의 경우 최단거리가 아닌 k 번 이내 방문에 대한 경로를 도출해내야하고 같은 격자를 여러번 방문할 수 있기 때문에, 이러한 조건들에 대해 별도 처리를 해줌으로써 최대한 시간복잡도를 다익스트라의 시간복잡도에 가깝도록 조치해줄 필요가 있음
    - 도착지점에 도착했을 때, 남은 이동가능 횟수가 2의 배수여야함(갔다 돌아오기 (예: d→u, l→r등))
    - 다음 지점을 탐색할 때, 해당 지점과 도착지점의 거리가 k보다 크면 어차피 탈출 불가능한 경로임(모두 거리가 1칸이기 때문에 지점 간 거리가 이동거리와 동일함)

내 코드

import heapq

def solution(n, m, x, y, r, c, k):
    q = []
    dr = ['d', 'l', 'r', 'u']
    dx = [1, 0, 0, -1]
    dy = [0, -1, 1, 0]

    heapq.heappush(q, ('', x, y))

    while q:
        path, x, y = heapq.heappop(q)
        if x == r and y == c:
            if len(path) == k:
                return path
            elif (k - len(path)) % 2:
                return 'impossible'
        for i in range(4):
            next_x = x + dx[i]
            next_y = y + dy[i]
            if next_x <= 0 or next_x > n or next_y <= 0 or next_y > m:
                continue
            if abs(next_x - r) + abs(next_y - c) + len(path) > k:
                continue
            heapq.heappush(q, (path+dr[i], next_x, next_y))
    return 'impossible'

코딩테스트 연습 - 부대복귀

문제요약

각 지역은 유일한 번호로 구분

두 지역간 통과 시 걸리는 시간은 모두 1

흩어진 부대원들이 최단시간 내에 부대가 있는 지역으로 복귀하고자 함

단, 경로가 없어져서 복귀 불가능한 대원이 있음

총 지역 수 n, 두 지역 왕복길 정보 roads, 각 부대원 위치 정보 sources, 지역 destination → sources 순서대로 복귀 가능한 최단 시간 배열을 구하고 복귀가 불가능하면 -1로 표기하라

입력

3≤ n ≤ 10^5

(각 지역 : 정수 1부터 n까지 번호로 구분)

2≤roads [[a,b], …] 2차원 배열 ≤ 5*10^5

([a,b] : a, b가 왕복가능)

1≤sources≤5*10^2

1≤destination≤n

출력

return int[]

어떻게 풀까?

  • 다른 지역에 있는 부대원들이 모두 부대가 있는 한 지역으로 최단 시간 내에 가야함 ⇒ 거꾸로 생각해보면, 한 지점에서 모든 지점으로 가야함 ⇒ 다익스트라 문제 ⇒ 다익스트라의 시간복잡도는 O(nlogn)이므로 가능
  • 다익스트라로 모든 지역에서 부대 복귀하는 최단distance 구한 뒤, sources를 돌아가면서 해당하는 distance 값을 answer 배열에 넣기 (단, 무한대(불가능)일 경우, -1 넣기)
  • 지역번호는 1부터 n인 점 유의!

내 코드

import heapq

def solution(n, roads, sources, destination):
    answer = []

    INF = int(1e9)
    graph = [[] for _ in range(n + 1)]
    for road in roads:
        a, b = road
        graph[a].append(b)
        graph[b].append(a)
    distance = [INF]*(n+1)
    distance[destination] = 0
    q = []
    heapq.heappush(q, (0, destination))

    while q:
        dist, now = heapq.heappop(q)
        if dist > distance[now]:
            continue
        if not graph[now]:
            continue
        for next in graph[now]:
            cost = dist + 1
            if cost < distance[next]:
                distance[next] = cost
                heapq.heappush(q, (cost, next))

    for region in sources:
        if distance[region] == INF:
            answer.append(-1)
        else:
            answer.append(distance[region])

    return answer

코딩테스트 연습 - 유사 칸토어 비트열

문제요약

칸토어 집합이란, [0,1] 부터 시작해서 각 구간을 3등분 한 후에 가운데 구간을 반복적으로 제외하는 방식

다음과 같이 유사칸토어 비트열을 생성함

  • 0 번째 유사칸토어 비트열 : 1
  • n 번째 유사칸토어 비트열 : n-1 번째 비트열에서 1을 ‘11011’로 치환하고 0을 ‘00000’로 치환

n 번째 유사칸토어 비트열에서 특정 구간 내의 1의 개수가 몇개인지 return하라

입력

1≤ n ≤ 20

1≤ l, r ≤ 5^n

  • l ≤ r < l + 10^7
  • ㅣ과 r은 비트열에서 인덱스(1-base)이며, 폐구간 [l, r]을 나타냄

출력

return int

어떻게 풀까?

  • 주의할 점 : l, r 이 1로 시작하는 인덱스이며 l과 r을 포함하는 구간으로 구해야 함
  • 1≤ l, r ≤ 5^n 및 l ≤ r < l + 10^7 조건으로 인하여, 시간복잡도를 고려한 알고리즘 구현 필요
  • 칸토어 집합 자체가 규칙이 있음 ⇒ 손으로 풀어보며 규칙을 찾아보자

⇒ 길이에 따라서 일정한 유사성을 가진 비트열이 생성되고 길이에 따라서 4의 배수로 1의 개수를 구할 수 있음

⇒ r-1번(index-0)까지의 1의 개수에서 l-2번(index-0)까지 1의 개수를 제외해주면 될 것

⇒ 이 때, 1의 개수는 해당 길이를 통해서 n번째 비트열에 속하는 지를 구한 후에 n-1번째 비트열의 길이로 그룹을 구하고 그 그룹의 개수만큼 4의 n-1승을 활용하여 구하면 됨

(단, 그룹의 수가 3개 이상일 때는 0이 5^(n-1)개 있는 그룹이 3번째 구간에 있으므로 그만큼을 제외해주어야함)

나머지의 경우, 재귀를 활용하여 처리

⇒ 현재, n을 5로 나누어주는 것으로 계속 진행을 하고 있기 때문에, 재귀의 종료조건은 n이 5이하가 될 때로 설정

(이 때는, '11011’[:길이]에서의 1의 개수를 반환하면 될 것)

내 코드

def get_power_of_base5(num):
	power = 1
	while 5**(power+1) < num:
	power += 1
	return power

def get_1_cnt(num):
	res = 0
	
	if num <= 5:
	    return '11011'[:num].count('1')
	
	power = get_power_of_base5(num)
	
	group = num // (5 ** power)
	
	res += group*(4** power)
	
	if group == 2:
	    return res
	elif group >= 3:
	    res -= 4 ** power
	
	remain = num % (5 ** power)
	if remain:
	    return res + get_1_cnt(remain)
	
	return res

def solution(n, l, r):
	return get_1_cnt(r) - get_1_cnt(l-1)

어려웠던 점

  • 처음에 시간복잡도를 미처 고려하지 못한 탓에 아래와 같은 코드를 작성하였고 시간초과 오류가 발생하였다.
def solution(n, l, r):
    ans = 0

    r -=1
    l -=1

    memo = []
    memo.append('1')
    for i in range(1, n+1):
        memo.append(memo[i-1]*2+'0'*5**(i-1)+memo[i-1]*2)

    r_group = (r//5**(n-1))

    if r_group <= 2 :
        ans += r_group*(4**(n-1))
    else:
        ans += (r_group-1)*(4**(n-1))

    for i in range(r_group*(5**(n-1)), r+1):
        if memo[n][i] == '1':
            ans += 1

    l_group = ((l//5**(n-1)))
    if l_group <= 2:
        ans -= l_group*(4**(n-1))
    else:
        ans -= (l_group-1)*(4**(n-1))

    for i in range(l_group*(5**(n-1)), l):
        if memo[n][i] == '1':
            ans -= 1

    return ans
  • 답을 찾는 데, n번째라는 인자값은 굳이 필요하지 않았다. 오히려 이 n번째 인자값을 어떻게 사용할지 고민하는 과정이 혼란을 주었던 것 같다. 주어지는 모든 인자들이 문제 해결에 필요하지 않을 수 있다는 점을 명심하자.
  • 재귀함수를 사용할 때, num이 1이 되는 조건을 처음에 걸었었는데 함수 내부에서 num을 5로 나누어 진행을 하다보니 이 조건으로 인하여 무한재귀가 발생하였다. 인자값을 어떻게 처리하는 지 잘 확인하고 해당 처리를 거치지 못하는 값에 대해서 종료조건이나 예외처리를 해주어야할 것이다.
  • 처음에 group 개수에 대한 1의 개수를 res에 반영함에 있어서 아래와 같이 처리하여 group이 1개이고 나머지가 있을 때를 제대로 처리하지 못했다.
def get_1_cnt(num):
...(중략)...

if group <= 2:
    return group*(4** power)
elif group >= 3:
    res -= 4 ** power
if remain:
        return res + get_1_cnt(remain)
return res
  • 전체적으로 처리를 한 후에, 일부 예외사항을 반영하는 아래와 같은 방식으로 변경하였다.
def get_1_cnt(num):
...(중략)...

res += group*(4** power)

if group == 2:
    return res
elif group >= 3:
    res -= 4 ** power

if remain:
        return res + get_1_cnt(remain)
return res

코딩테스트 연습 - 마법의 엘리베이터

문제요약

절대값이 10^c 형태인 엘리베이터 버튼

버튼 누르면 현재 층수 + 버튼 값으로 이동

(단, 합이 0 보다 작으면 움직이지 않음)

0층 : 가장 아래층

현재 엘베층 : 민수가 있는 층

버튼 1번에 마법의 돌 1개 소모

어떤 층에서 0층으로 갈 때, 필요한 돌의 개수?

입력

1 ≤ 엘베 층수 storey ≤ 10^8

출력

return int

어떻게 풀까?

  • +/- 양측으로 조정 가능

⇒ 각 자리수 별로 0에 가까운지 10에 가까운지 구분해서 10-n/n 값 중 작은 것을 카운팅해주면 될 것!

⇒ 1의 자리에서 가장 높은 단위까지로 반복문으로 수행(10^8이지만, 9자리이므로 충분)

 

  • 이 때, 10에 가깝다면 윗 자리 값에 +1을 해주기

⇒ 아래 단위에서 버튼 한 번 누르나, 윗 단위에서 한 번 누르나 버튼 누른 횟수 카운팅은 동일

⇒ 가장 높은 자리의 경우, 별도 처리 필요(10에 가깝다면 10-n을 카운팅 해주고 그 값이 올림이 되었을 때를 고려하여 +1을 추가 카운팅해주기)

 

  • 단, 0이나 10에 대하여 동일하게 가까운 5의 경우는 별도 고려 필요할 것!

⇒ 이 때에는 윗자리가 5이상인지 조건을 걸어, 5이상이면 올려주고 미만이면 내려주는 방식으로 처리 필요

내 코드

def solution(storey):
    storey_list = list(map(int, str(storey)))
    ans = 0

    for i in range(len(storey_list)-1, -1, -1):
        if 10 - storey_list[i] > storey_list[i]:
            ans += storey_list[i]
        elif storey_list[i] == 5:
            ans += 5
            if storey_list[i-1] >= 5:
                storey_list[i-1] += 1
        else:
            ans += 10 - storey_list[i]
            if i == 0:
                ans += 1
            else:
                storey_list[i-1] += 1
    return ans

(학습정리 자료)   Front-End 테스트에 대한 학습정리  Lighthouse 학습정리  크롬개발자 도구 학습정리

검사 결과

화면

검사화면

Lighthouse 결과

Lighthouse결과

접근성 검사 상세

성능 검사 점수 상세

성능점수 상세

성능 검사 추천 및 진단

추천 및 진단

성능 패널

성능패널1
성능패널2

검사결과 분석

성능 부문 개선사항

[로고이미지 width, height 지정 필요]

왜 로고이미지 width, height가 필요한 걸까?

  • 화면의 랜더링 과정에서, 레이아웃 계산은 한 번만 일어나지 않는다.
  • 레이아웃 과정에서 요소들은 다른 요소들의 배치에도 영향을 받고 재계산 과정이 일어나므로, 보통 랜더링 절차 중 많은 비용이 든다.
  • 이미지의 경우, 이미지가 다운로드되기 시작하고 브라우저가 크기를 결정할 수 있을 때만 이미지에 대한 공간을 할당할 수 있기 때문에, 이미지가 로드되면 각 이미지가 화면에 나타날 때 페이지의 재배치가 발생한다.
  • Cumulative Layout Shift(누적 레이아웃 이동, CLS) 의 발생원인
    • 크기가 정해지지 않은 이미지
    • 크기가 정해지지 않은 광고, 임베드 및 iframe
    • 동적으로 주입된 콘텐츠
    • FOIT/FOUT을 유발하는 웹 글꼴
    • DOM을 업데이트하기 전에 네트워크 응답을 대기하는 작업
  • 참고자료1, 참고자료2

개선방향

이미지 요소에 width및 height크기 속성을 포함시켜주자!

접근성 부문 개선사항

[버튼 이름 설정]

왜 버튼에 이름이 필요할까?

  • Screen reader(컴퓨터의 화면 낭독 소프트웨어)를 위해서이다.
  • Screen reader 사용자들은 접근가능한 이름이 없는 role="link", role="button", role="menuitem" 요소의 목적을 구분할 수 없다.
  • 버튼에는 화면 판독기 사용자를 위한 대상, 목적, 기능 또는 작업을 명확하게 설명하는 식별 가능한 텍스트가 필요하다.

개선방향

아래의 button-name 규칙을 참고하여, 버튼에 네이밍을 해주자!

  • button-name 규칙
    각 button요소와 요소 role="button"에 다음 특성 중 하나가 있는지 확인한다.
    • 화면 판독기 사용자가 식별할 수 있는 내부 텍스트
    • 비어 있지 않은 aria-label속성
    • aria-labelledby 을 이용하여, 스크린 리더 사용자가 식별할 수 있는 텍스트가 있는 요소를 가리킴
    • role="presentation"또는 role="none"(ARIA 1.1) 단, tab order가 아님(tabindex="-1")
    5가지 markup pattern 예시
    <button id="al" aria-label="Name"></button>
    <button id="alb" aria-labelledby="labeldiv"></button>
    <div id="labeldiv">Button label</div>
    <button id="combo" aria-label="Aria Name">Name</button>
    <button id="buttonTitle" title="Title"></button>
  • <button id="text">Name</button>
  • 참고자료

 

(참고) https://developer.chrome.com/docs/devtools/

Network 패널

  • 네트워크 패널은 자원들이 제대로 다운로드 되었는지의 여부, 캐시여부, 그리고 다운로드된 자원들의 다운로드에 걸린 시간, 세부 정보들을 보고 싶을 때 유용하게 사용할 수 있는 패널임
    • 리소스가 실제로 업로드 또는 다운로드되고 있는지 확인
    • HTTP 헤더, 콘텐츠, 크기 등과 같은 개별 리소스의 속성을 검사
  • 네트워크 활동과 관련이 없는 많은 유형의 로드 성능 문제가 있기 때문에, Lighthouse나 Performance패널을 활용하자
    • 페이지를 더 빠르게 로드하는 방법은 Lighthouse를 참조하자.
    • 성능패널의 런타임 성능은 페이지 로드 중이 아닌 실행 중일 때 성능
    • 참고로 성능분석을 실시할 때는 시크릿 모드에서 수행하는 것이 좋음 (시크릿 모드는 Chrome이 깨끗한 상태로 실행되도록 함. 예를 들어 많은 확장 프로그램이 설치된 경우 해당 확장 프로그램으로 인해 성능 측정에 노이즈가 발생할 수 있음)

Performance 패널

  • 페이지가 로드되는 것이 아니라 실행되는 동안 페이지의 성능을 분석하려는 경우 런타임 성능을 기록함

모바일 CPU 시뮬레이션

  • 모바일 장치는 데스크톱 및 랩톱보다 CPU 성능이 훨씬 낮음
  • 페이지를 프로파일링할 때마다 CPU 스로틀링을 사용하여 모바일 장치에서 페이지가 수행되는 방식을 시뮬레이션해보자.
  1. DevTools에서 성능 탭을 클릭
  2. Screenshots 체크박스가 활성화 되어 있는지 확인
  3. Capture Settings을 클릭 합니다
  4. CPU 의 경우 2x slowdown 을 선택(DevTools는 CPU를 평소보다 2배 느리게 조절)참고 : 다른 페이지를 테스트할 때 저사양 휴대기기에서 제대로 작동하는지 확인하려면 CPU Throttling을 20x slowdown 으로 설정

성능 분석 하는 방법

  • 성능 분석은 웹 페이지가 동작하고 있을 때 자동으로 되는 것이 아니라 특정 구간을 녹화한 후 그 구간을 분석해서 수행하는 식으로 이루어짐
    • 초기 렌더링 성능을 분석하고 싶다면 웹페이지를 불러오기 전에 녹화 버튼을 누르거나 새로고침 버튼을 누르면 됨

1. DevTools에서 Record를 클릭 → DevTools는 페이지가 실행될 때 성능 메트릭을 캡처

2. 몇 초만 기다려보자.

3. 중지 를 클릭 → DevTools는 기록을 중지하고 데이터를 처리한 다음 성능 패널에 결과를 표시

분석결과의 의미

초당 프레임 분석

모든 애니메이션의 성능을 측정하는 주요 메트릭은 초당 프레임 수(FPS) : 사용자는 주로 애니메이션이 60FPS로 실행될 때 만족함

 

1. FPS 차트에서 FPS 위에 빨간색 막대가 표시될 때마다 프레임 속도가 너무 낮아져 사용자 경험에 해를 끼칠 수 있음을 의미하고 일반적으로 녹색 막대가 높을수록 FPS가 높음

그림 1 파란색 윤곽선으로 표시된 FPS 차트

 

2. FPS 차트 아래 에 CPU 차트가 표시됨.  CPU 차트의 색상은 성능 패널 하단에 있는 요약 탭의 색상에 해당 CPU 차트가 색상으로 가득 하다는 사실 은 기록 중에 CPU가 최대로 사용되었음을 의미함 → 오랜 기간 동안 CPU가 최대치에 도달한 것을 볼 때마다 작업을 덜 수행할 수 있는 방법을 찾아야 한다는 신호임

그림 2 파란색 윤곽선으로 표시된 CPU 차트 및 요약 탭

3. FPS , CPU 또는 NET 차트 위로 마우스를 가져가면, DevTools는 해당 시점의 페이지 스크린샷을 보여줌  녹화를 재생하려면 마우스를 좌우로 움직여보자 → 이를 스크러빙이라고 하며 애니메이션 진행을 수동으로 분석하는 데 유용함

그림 3 기록의 2000ms 표시 부근의 페이지 스크린샷 보기

4. 프레임 섹션 에서 녹색 사각형 중 하나 위로 마우스를 가져가면, DevTools는 특정 프레임에 대한 FPS를 보여줌(각 프레임은 아마도 목표인 60FPS보다 훨씬 낮을 것)

그림 4 프레임 위에 마우스 올리기

병목 현상 찾기

애니메이션이 제대로 작동하지 않는다는 것을 측정하고 확인했다면, 이유를 찾아야한다.

 

1. 요약 탭을 확인해보자. 이벤트를 선택하지 않으면 이 탭에 활동 내역이 표시됨  페이지는 대부분의 시간을 렌더링하는 데 사용했다. 

성능은 작업을 적게 하는 기술이므로 렌더링 작업에 소요되는 시간을 줄이는 것이 목표임

그림 1 요약 탭(파란색 윤곽선)

2. 기본 섹션을 확장해보자.  DevTools는 시간 경과에 따른 기본 스레드 활동의 화염 차트를 보여준다. 

x축은 시간 경과에 따른 기록을 나타내고 각 막대는 이벤트를 나타냄 → 넓은 막대는 이벤트가 더 오래 걸렸음을 의미 

y축은 호출 스택을 나타냄 →  이벤트가 서로 쌓여 있는 것을 보면 상위 이벤트가 하위 이벤트의 원인이 되었음을 의미

그림 2 파란색 외곽선으로 표시된 메인 섹션

3. 기록에 많은 데이터가 있다.  FPS , CPU 및 NET 차트 가 포함된 섹션인 개요 위로 마우스를 클릭한 상태로 드래그하여 단일 애니메이션 프레임 실행 이벤트를 확대해보자. → 기본 섹션 및 요약 탭 에는 선택한 녹음 부분에 대한 정보만 표시됨

그림 3 단일 Animation Frame Fired 이벤트 확대

(참고 : 확대/축소하는 또 다른 방법 은 배경을 클릭하거나 이벤트를 선택하여 메인 섹션에 초점을 맞춘 다음 W, A, S 및 D 키를 누르는 것)

 

4. Animation Frame Fired 이벤트 의 오른쪽 상단에 있는 빨간색 삼각형에 주목해보자. → 빨간색 삼각형이 표시될 때마다 이 이벤트와 관련된 문제가 있을 수 있다는 경고임
(참고 : Animation Frame Fired[requestAnimationFrame()](<https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame>) 이벤트는 콜백이 실행될 때마다 발생)

 

5. Animation Frame Fired 이벤트를 클릭해보자 이제 요약 탭에 해당 이벤트에 대한 정보가 표시된다. → 클릭하면 DevTools가 Animation Frame Fired 이벤트 를 시작한 이벤트를 강조표시함 

또한 app.js:94 링크를 클릭하면 소스 코드의 관련 줄로 이동함

그리고 원형 차트가 위치했던 summary 탭이 갱신되면 'reveal' 이라 표시된 링크가 생기고 이 링크를 클릭하면 이를 통해 정확히 어느 위치의 어떤 코드가 해당 이벤트를 발생시키는지를 추적할 수 있음

그림 4 Animation Frame Fired 이벤트에 대한 추가 정보

(참고 : 이벤트를 선택한 후 화살표 키를 사용하여 옆에 있는 이벤트를 선택)

 

6. app.update 이벤트 아래에는 많은 보라색 이벤트가 있을 수 있는데(더 넓으면 각각 빨간색 삼각형이 있는 것처럼 보임), 보라색 레이아웃 이벤트 중 하나를 지금 클릭해보자.

DevTools는 요약 탭 에서 이벤트에 대한 자세한 정보를 제공함.

실제로 강제 리플로우(레이아웃의 또 다른 단어)에 대한 경고가 있음.

 

7. Summary 탭 에서 Layout Forced 아래의 app.js:70 링크를 클릭해보자. → DevTools는 레이아웃을 강제로 적용한 코드 줄로 이동함

그림 5 강제 레이아웃을 유발한 코드 라인

참고 : 이 코드의 문제점은 각 애니메이션 프레임에서 각 사각형의 스타일을 변경한 다음 페이지에서 각 사각형의 위치를 쿼리한다는 것임. 스타일이 변경되었기 때문에 브라우저는 각 사각형의 위치가 변경되었는지 알지 못하므로 위치를 계산하기 위해 사각형을 다시 배치해야 함.(참조 :  강제 동기 레이아웃 방지)

 

성능을 이해하기 위한 Rail 모델

  • RAIL은 성능에 대해 생각할 수 있는 구조를 제공하는 사용자 중심 의 성능 모델임
  • 이 모델은 사용자 경험을 주요 작업(예: 탭, 스크롤, 로드)으로 분류하고 각각에 대한 성능 목표를 정의하는 데 도움을 줌
  • RAIL은 웹 앱 수명 주기의 4가지 뚜렷한 측면인 응답(response), 애니메이션(animation), 유휴 상태(idle) 및 로드(load)를 나타내고 사용자는 이러한 각 컨텍스트에 대해 서로 다른 성능 기대치를 가지고 있으므로 컨텍스트 및 사용자가 지연을 인식하는 방식에 대한 UX 연구를 기반으로 성능 목표가 정의됨
  • 참고 : https://web.dev/rail/

런타임 성능 개선을 위한 팁

출처:
https://developer.chrome.com/docs/devtools/
https://codingmoondoll.tistory.com/entry/크롬-개발자-도구의-Performance-탭-다루기-기본편

Google Lighthouse란?

웹 페이지의 품질을 개선하기 위한 오픈 소스 자동화 도구

공개 또는 인증이 필요한 모든 웹 페이지에 대해 실행 가능

성능, 접근성 Progressive Web Apps, SEO 등에 대해 평가함

  • 성능 : 대화형 시간, 대기 시간, 속도 지수, 리소스 최적화, TTFB, 자산 전달, 스크립트 실행 시간, DOM 크기 등
  • SEO : 모바일 친화적, 메타, 크롤링, 표준, 구조 등
  • 모범사례 : 이미지 최적화, JS 라이브러리, 브라우저 오류 로깅, HTTPS를 통한 액세스, 알려진 JS 취약점 등
  • 접근성 : 페이지 요소, 언어, ARIA 속성 등
  • PWA : HTTP를 HTTPS로 리디렉션, 응답 코드 확인, 3G에서의 빠른 로딩, 스플래시 화면, 뷰포트 등

왜 Lighthouse를 써야하나?

  • 사용하기가 편하다.
  • Google에서 개발했다는 점에서 신뢰도가 있다.
  • 오픈소스이다.
  • 완전히 자동화되어 있다.
  • 스캔한 웹 페이지가 모바일 장치에서 어떻게 보이고 작동하는지도 테스트한다.

어떻게 쓸까?

Chrome 개발자 도구

이것이 이 문서의 메트릭에 대한 스크린샷을 만든 방법입니다.:

  1. 감사할 페이지로 이동합니다.
  2. DevTools(Windows에서는 Ctrl+Shift+I 또는 F12, Mac에서는 Cmd+Option+I)를 엽니다.
  3. 감사 탭으로 이동합니다.
  4. 감사 수행을
  5. 클릭 하고 원하는 범주를 선택합니다.
  6. 감사를 실행합니다.

이는 사용자 인증이 필요한 페이지를 테스트할 때 특히 유용할 수 있습니다.

여기서 흥미로운 점은 Lighthouse 를 Google Chrome 뿐만 아니라 일부 Chromium 기반 브라우저에서도 사용할 수 있다는 것입니다. 예를 들어, 아래는 현재 Google Chrome과 동일한 엔진을 사용하는 최신 버전의 Microsoft Edge에서 가져온 Lighthouse 감사의 스크린샷입니다.

Microsoft Edge 브라우저에서 시작된 Google Lighthouse 감사의 스크린샷.

Lighthouse를 노드 모듈로 실행

이렇게 하면 명령줄에서 감사를 실행하고 감사 결과가 포함된 * .html 파일을 얻을 수 있습니다.

  1. 컴퓨터에 Google 크롬이 설치되어 있는지 확인하세요.
  2. Node의 현재 장기 지원 버전을 설치합니다 (다음 예제는 최신 비 LTS 버전으로 수행되었지만 Google 자체에서 LTS 버전 사용을 권장함).
  3. 다음 명령을 사용하여 Lighthouse를 전체적으로 설치합니다. npm install -g lighthouse
  4. lighthouse 명령으로 " https://google.com" 데모 감사를 실행해 봅시다 .
  5. 감사 목적으로 Chrome 창이 자동으로 나타납니다. 생성된 보고서는 현재 폴더에 저장됩니다.

감사를 실행할 때 Lighthouse는 현재 테스트 단계(왼쪽)를 인쇄하고 완료되면 보고서 데이터(오른쪽)가 포함된 HTML 파일을 생성합니다.

프로그래밍 방식으로 Lighthouse 모듈 실행

통합 프로세스를 실행 중인 경우 Lighthouse를 모듈로 사용하여 자동화된 테스트를 실행할 수 있습니다. 예를 들면 다음과 같습니다.

const fs = require('fs');
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');

(async () => {
  const chrome = await chromeLauncher.launch({chromeFlags: ['--headless']});
  const options = {logLevel: 'info', output: 'html', onlyCategories: ['performance'], port: chrome.port};
  const runnerResult = await lighthouse('<https://example.com>', options);

  // `.report` is the HTML report as a string
  const reportHtml = runnerResult.report;
  fs.writeFileSync('lhreport.html', reportHtml);

  // `.lhr` is the Lighthouse Result as a JS object
  console.log('Report is done for', runnerResult.lhr.finalUrl);
  console.log('Performance score was', runnerResult.lhr.categories.performance.score * 100);

  await chrome.kill();
})();

성능지표?

지각적으로 페이지의 속도를 측정

(해당 측정항목 및 해당 가중치)

  • First Contentful Paint—15%
  • Speed Index—15%
  • Largest Contentful Paint—25%
  • Time to Interactive—15%
  • Total Blocking Time—25%
  • Cumulative Layout Shift—5%

First Contentful Paint

  • FCP라고 하는 이 지표는 브라우저가 DOM 콘텐츠를 렌더링하는 데 걸리는 시간을 보여줌
  • 이 경우 DOM 콘텐츠는 텍스트, 이미지, 흰색이 아닌 <canvas>요소 및 SVG
  • 이 측정항목은 로드 시간이 아니라 렌더링 시간을 표시 한다는 점을 이해하는 것이 중요
    • 예를 들어, 브라우저가 특정 텍스트를 로드했지만 글꼴이 여전히 네트워크를 통해 이동 중인 경우 여전히 콘텐츠가 있는 페인트로 간주되지 않는다.
  • 이 도구는 색상 코딩 시스템을 사용하여 특정 메트릭에 따라 페이지가 얼마나 잘 수행되는지 표시함.
    • 메트릭 근처의 원은 빨간색, 주황색 또는 녹색(각각 느림, 보통 또는 빠름)일 수 있습니다. 해석은 다음과 같다.
    • 2초 미만의 FCP 시간 - 녹색, 빠름
    • 2~4초의 FCP 시간—주황색, 보통
    • 4초 이상의 FCP 시간 - 빨간색, 느림

속도지수

  • 이것은 페이지의 콘텐츠가 시각적으로 얼마나 빨리 로드되는지 보여줌
  • 이를 위해 Lighthouse는 페이지 로드 비디오를 녹화한 다음 프레임 간의 시각적 진행을 계산함
  • 따라서 페이지 요소가 빠르게 표시되는 경우(하지만 여전히 백그라운드에서 로드되는 스크립트가 있을 수 있음) 이 메트릭은 안전 영역에 있습니다.
  • Lighthouse는 이 지표를 사용하여 페이지 속도를 다른 웹사이트와 비교하며, 조건은 다음과 같다.
    • 4.3초 미만의 속도 지수 - 녹색, 빠름
    • 4.4~5.8초의 속도 지수—주황색, 보통
    • 5.8초 동안의 속도 지수—빨간색, 느림

Largest Contentful Paint

  • LCP는 성과 점수 계산기에서 중요한 역할을 함
  • 뷰포트(즉, 페이지의 보이는 부분) 내에서 가장 큰 이미지 또는 텍스트 블록 의 렌더링 시간을 보고함
  • 메트릭을 트리거할 후보로 간주되는 요소:
    • <img>집단
    • <image><svg> 요소 내부 의 요소
    • <video>요소(이 경우 포스터 이미지 사용)
    • CSS에서 url()을 통해 로드된 배경 이미지가 있는 요소
    • 자식이 있는 블록 수준 요소(텍스트 노드도 포함) - 여백, 패딩 및 테두리는 무시됨
  • 가장 큰 요소는 페이지가 점진적으로 로드됨에 따라 변경될 수 있고 가장 큰 요소 캡처의 마지막 레코드가 보고됨
    • LCP 2.5초 미만 - 녹색, 빠름
    • 2.5~4초의 속도 지수—주황색, 보통
    • 4초 동안의 속도 지수—빨간색, 느림

Time To Interactive

  • 페이지가 완전히 상호 작용 하는 데 걸리는 시간을 측정
  • 이 경우 "완전히"의 의미는 다음과 같다.
    • 유용한 콘텐츠(FCP로 측정)가 표시됨
    • Javascript 이벤트 핸들러는 보이는 요소의 이벤트에 바인딩됨
    • 페이지는 50밀리초 이내에 사용자 상호 작용에 응답함
  • 비교 데이터는 HTTP Archive 에서 가져옴
    • 3.9초 미만의 TTI - 녹색, 빠름.
    • 3.9~7.3초의 TTI—주황색, 보통
    • 7.3초 이상의 TTI—빨간색, 느림

Total Blocking Time

  • 페이지가 50밀리초 이상 사용자 상호 작용에서 차단될 때 FCP와 TTI 사이의 모든 시간 기록의 합계
    • 예를 들어 일부 Javascript 코드가 70밀리초 동안 페이지 로드를 중단하면 TBT가 20밀리초 증가함
  • Lighthouse의 소스 코드에 따르면 TBT 점수는 상위 10,000개 웹사이트와 비교하여 계산됨
    • 300밀리초 미만의 TBT - 녹색, 빠름.
    • 300~600밀리초의 TBT—주황색, 보통
    • 600밀리초 이상의 TBT - 빨간색, 느림

Cumulative Layout Shift

  • CLS는 요소가 서로 얼마나 적극적으로 이동하는지 알려줄 수 있음
    • 예를 들어 긴 텍스트를 읽고 있다고 가정할 때, 읽고 있는 부분 위에 요소가 렌더링되면 텍스트가 아래로 이동함. 이와 같은 상황을 제거하면 CLS 점수에 긍정적인 영향을 줌
  • 이 지표는 예상치 못한 레이아웃 변경만 고려하며, 예상치 못한 레이아웃 변경은 변경 전 500ms 시간 프레임 내에 사용자 입력이 없는 레이아웃 변경입니다.

CI?

(참고) GoogleChrome/lighthouse-ci GitHub

  • 팀에서 지속적 통합 워크플로를 사용하는 경우 Google Lighthouse CI 도구 세트를 사용하여 워크플로의 일부로 Google Lighthouse를 실행할 수 있음
  • Google Lighthouse CI는 CI 워크플로우에서 Google Lighthouse 점수 실행을 단순화하는 도구 모음임
  • Lighthouse CI는 Circle CI, GitHub 작업 및 Travis CI와 같은 CI 공급자와 함께 작동함

1단계 - GitHub Actions 워크플로 디렉터리

  • .github/workflowGitHub 작업 사용을 시작하려면 프로젝트 디렉터리의 루트에 GitHub 워크플로 파일을 저장할 디렉터리를 만들어야 한다.

2단계 - GitHub Actions 워크플로 파일

  • 특정 이벤트가 발생할 때 Google Lighthouse CI를 실행하기 위한 코드를 포함할 YAML 워크플로 파일을 만든다.
  • 이벤트가 발생 (push, pull_request)하면 Google Lighthouse CI를 실행 하도록 하자.
  • lighthouse.yaml 이라는 YAML 파일을 만들자.
  • 프로젝트 디렉터리의 루트에 HTML 파일이 있는 가장 간단한 경우를 살펴보자.
  • 또한 프로젝트 디렉터리의 루트에 configuration file이 없다고 가정한다.
  • 방금 만든 YAML 파일에 아래 코드를 복사하여 붙여넣어보자.
  • 변경 사항을 Git 리포지토리로 푸시하거나 풀 요청을 열 때마다 코드가 실행된다.
  • 프로젝트에 빌드 단계가 필요한 경우엔 워크플로 파일을 약간 수정할 수 있다.
name: Run lighthouse CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lhci:
    name: Lighthouse CI
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x]

    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - name: Run lighthouse CI
        run: |
          npm install -g @lhci/cli@0.3.x && lhci autorun --upload.target=temporary-public-storage --collect.staticDistDir=./

3단계 - Push the changes to GitHub

  • 위의 코드는 변경 사항을 커밋하고 GitHub에 푸시할 때 Google Lighthouse CI를 실행한다.
  • 누군가 풀 요청을 열거나 커밋을 푸시할 때마다 GitHub 작업은 지정된 명령을 실행하므로 시간 경과에 따른 회귀를 알 수 있다.
  • Circle CI 및 Travis CI와 같은 다른 CI 도구와 함께 Google Lighthouse를 실행할 수 있으며, Google Lighthouse CI 문서는 지원되는 모든 CI 제공자를 안내하고 있다.

참고자료

사이트 성능을 높이기 위한 기술들을 다룬 블로그
https://web.dev/fast/
https://yceffort.kr/2021/08/javascript-tree-shaking

출처 :
개요
https://developer.chrome.com/docs/lighthouse/overview/
성능지표 설명 및 사용법
https://uploadcare.com/blog/what-is-google-lighthouse/#largest-contentful-paint
사용 시 장점
https://medium.com/@OPTASY.com/how-good-of-website-analysis-tool-is-google-lighthouse-6-reasons-to-use-it-e86adc5d505b
CI
https://refine.dev/blog/lighthouse-google-chrome/

왜 반응형 레이아웃 적용이 필요했나?

진행하고 있는 프로젝트의 경우, 프로필박스에서 코드를 보여주는 것이 중요하기에 사이즈가 무한정 늘어나거나 줄어들어 프로필박스의 코드를 알아보는데 지장을 주는 것은 프로젝트가 제공하는 서비스에 올바를 방향이 아니었고 미관상으로도 좋지 않았다.

[기존 해결방식]

해결한 방법?

이에, css적으로 해결하려 하였다.

화면 사이즈에 맞게 width를 계산하도록 하였고 minWidth 값과 maxWidth값을 주어 해결하려고 했다.

여전히 남아있었던 문제점?

여전히 디자인 상 문제점이 남아있었습니다. 사이즈가 줄어들어서 프로필박스가 한 행에 2개씩 나오게 될 경우에 프로필박스의 개수가 홀수라면, 마지막 하나의 프로필 사이즈는 다른 프로필 사이즈와 다르게(넓어짐) 나타난다는 것이었다.

해결방법?

가장 간단하게 해결할 수 있는 방법을 고민해보았다.

메인화면에서 한 행에 표현될 수 있는 프로필박스 개수의 경우는 3개, 2개, 1개 이렇게 3가지가 있고 문제가 되는 경우는 2개인 경우이다.

이에, 2개가 나오는 경우의 브라우저 사이즈를 측정하였고 window.addEventListener의 'resize'이벤트를 통해 한 행에 2개씩 표현이 되는 경우에는 빈 프로필 박스를 임의로 추가해줌으로써 간단히 추가적인 미관상 문제를 해결할 수 있었다.

/** @jsxImportSource @emotion/react */

import { useCallback, useEffect, useRef, useState } from 'react';

import Profile from './Profile';
import { singleProfileData } from './types';

import { emptyProfileBoxStyle, profileListStyle } from './styles';
import { COMMON_SIZE } from 'styles/sizes';

interface Props {
  profileData: Array<singleProfileData>;
}

const ProfileList = ({ profileData }: Props) => {
  const profileListRef = useRef<HTMLDivElement>(null);
  const [isOdd, setIsOdd] = useState<boolean>(false);
  const [isBlankNeeded, setIsBlankNeeded] = useState<boolean>(false);

  useEffect(() => {
    if (profileData.length % 2 !== 0) setIsOdd(true);
    else setIsOdd(false);
  }, []);

  const isWidthDouble = useCallback((targetWidth: number) => {
    return COMMON_SIZE.PROFILELIST_SINGLE_WIDTH < targetWidth && targetWidth < COMMON_SIZE.PROFILELIST_TRIPLE_WIDTH;
  }, []);

  const decideBlank = useCallback(() => {
    if (!profileListRef.current) return;
    if (isWidthDouble(profileListRef.current.clientWidth)) setIsBlankNeeded(true);
    else setIsBlankNeeded(false);
  }, [profileListRef.current]);

  useEffect(() => {
    window.addEventListener('resize', decideBlank);
    return () => {
      window.removeEventListener('resize', decideBlank);
    };
  }, []);

  return (
    <div css={profileListStyle} ref={profileListRef}>
      {profileData.map((data) => (
        <Profile key={`profile-${data.id}`} singleData={data} />
      ))}
      {isOdd && isBlankNeeded && <div css={emptyProfileBoxStyle} />}
    </div>
  );
};

export default ProfileList;

결과물

[최종 해결방안]

변경 사유

프로필 개수 외에도 필터링 바 등 화면 사이즈에 따라, 디자인 변경이 필요했다.
또한, 프로필박스 개수가 4개, 5개 등인 경우에 기존 방식으로는 디자인에 예외사항이 발생했다.

변경 사항

미디어쿼리를 사용하여, 반응형 레이아웃 디자인(필터링 바 등)을 좀 더 다듬었다.
프로필박스를 사이즈에 맞게 보여주는 방법도 빈 박스를 추가하는 방식에서, 미디어 쿼리에 따라 width를 조정하는 방식으로 반영하였다.

성과

불필요한 resize이벤트를 제거할 수 있었다.
다양한 프로필 개수에 대해서도 동일한 레이아웃을 보장할 수 있었다.

결과물

+ Recent posts