hexo+icarus 다크모드 깜빡임 해결

예전에 메모해놓고 이제 게시글로 적는다!

기본 배경 색 없애기

먼저 css레벨에서 적용되는 css를 초기화해야한다.
브라우저는 기본적으로 css가 없을 시 페이지 이동할 때 이전 페이지의 배경색을 그대로 유지한다.

\theme\icarus\css\include\style\base.styl
7줄 기본 배경색 none으로 수정하기

1
$body-background-color ?= none;

기존 js파일을 두 개로 나누기

로컬스토리지를 읽고 다크모드를 적용하는 함수와 html을 전부 로드한 후 버튼의 innerText를 수정하는 함수로 나누었다.

다크모드를 적용하는 함수는 render blocking이 이루어져야하고 버튼의 innerText의 수정은 HTML이 모두 로드 후 이루어져야하기 때문이다.

따라서 버튼을 수정하는 함수는 defer로 불러오고 다크모드를 확인하고 적용하는 함수는 속성 없는 script태그로 불러온다.

\themes\icarus\source\js\darkmode.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function init() {
const darkScheme = window.matchMedia("(prefers-color-scheme:dark)");
const theme = localStorage.getItem("theme");
if (theme === "dark") {
document.body.classList.add("dark");
return;
}
if (theme === "light") {
return;
}

if (darkScheme) document.body.classList.add("dark");

localStorage.setItem("theme", darkScheme ? "dark" : "light");
}

hexo\themes\icarus\source\js\darkmodebutton.js

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
const darkModeButton = document.querySelector("#changeModeButton");
const darkScheme = window.matchMedia("(prefers-color-scheme:dark)");

darkScheme.addEventListener("change", (e) => {
console.log("hidark");
const newColorScheme = e.matches ? "dark" : "light";
localStorage.setItem("theme", newColorScheme);

if (newColorScheme === "dark") {
document.body.classList.add("dark");
btn.innerText = "🌙";
return;
}
document.body.classList.remove("dark");
btn.innerText = "🌞";
});

function toggleTheme() {
console.log("hibutton");
document.body.classList.toggle("dark");
darkModeButton.innerText = document.body.classList.contains("dark")
? "🌙"
: "🌞";
localStorage.setItem(
"theme",
document.body.classList.contains("dark") ? "dark" : "light"
);
}

function initButton() {
console.log("initButton");
const data = localStorage.getItem("theme");
if (data == "dark") {
darkModeButton.innerText = "🌙";
return;
}
darkModeButton.innerText = "🌞";
}

initButton();
darkModeButton.addEventListener("click", toggleTheme);

\themes\icarus\layout\common\scripts.jsx
56번쨰 줄에 darkmodebutton.js추가

1
2
<script src={url_for("/js/main.js")} defer></script>
<script src={url_for("/js/darkmodebutton.js")} defer></script>

hexo\themes\icarus\layout\common\head.jsx

js를 로드하고 실행하는 동안 렌더링을 막아야하기 때문에 script에 속성을 지정하지 않아야한다.
적다보니 preload속성을 추가해도 좋을 것 같다.

1
<script src={url_for("/js/darkmode.js")}></script>

고의로 render blocking 만들기

새로고침이 되어서 다크모드가 적용되기 전 까지 렌더링이 이루어지면 안된다.

body의 최상단에 darkmode.js에서 정의한 init함수를 호출해준다.
hexo\themes\icarus\layout\layout.jsx

1
2
3
4
<html lang={language ? language.substr(0, 2) : ""}>
<Head site={site} config={config} helper={helper} page={page} />
<body class={`is-${columnCount}-column`}>
<script>init();</script>

ref

https://www.youtube.com/watch?v=ElsZ-v4Ow08

css float에 대하여

float 속성은 이미지를 배치하고 주변으로 텍스트를 흐르게 할 용도로 만들어졌다.

동시에 박스요소 끼리는 겹치게 만든다.

특징

  1. 플로팅 요소의 너비는 수축하고 일반적인 흐름에서 벗어난다.
  2. 인접 후행 블록 요소는 플로팅 요소와 겹치고 인라인 요소는 플로팅 요소 주변으로 흐른다.
  3. clear, flow-root 속성으로 해제할 수 있다.
    • clear는 블럭 요소에만 적용할 수 있다.
  4. 컬럼을 배치하는 속성이 아니다.

해제하는 법

해제하는 법에는 여러 방법이 있다.

  1. 부모 요소에 float 적용하기
  2. 자식 요소의 마지막에 빈 element에 clear 속성 추가하기
  3. 부모요소에 overflow:hidden 추가하기
  4. 부모요소의 display를 inline-block으로 바꾸기
  5. 부모요소에 after를 이용하여 clear속성 추가하기
  6. 부모요소를 display:flow-root를 추가하기

float과 display

float 속성이 들어간 요소는 display가 block이 된다.

display 속성이 없어도 float을 사용하면 width/height 수직 margin/padding을 사용할 수 있다.

columns <’column-width’> || <’column-count’>

columns 속성을 사용하면 div 하나에서 세 개의 column으로 만들 수 있다.

1
2
3
4
5
6
7
8
 {
columns: 310px 2;
column-gap: 20px;
column-rule: 20px solid #0002;

break-inside: avoid;
/* box 내부에서 자르지 않는다. */
}

https://flexboxfroggy.com/#ko
https://t.ly/2kOk

css 여백에 대하여

패딩과 마진은 부모요소의 너비를 기준으로 한다.

비율이 유지된 박스를 만들 수 있다.

종횡비 유지 비법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 영상을 뷰포트에 맞춤하는 경우 유용 */
iframe {
width: 100vw;
height: 56.25vw;
}
/*사파리, 삼성 인터넷 지원 확인 필요 */
iframe {
width: 100%;
height: auto;
aspect-ratio: 100/56.25;
}

.utube {
position: relative;
padding-top: 56.25%;
}

.utube__iframe {
position: absolute;
width: 100%;
height: 100%;
top: 0;
}

유용성

  1. 누적 배치 변경(CLS) 문제 해결
  2. 스켈레톤 UI를 제공할 때
  3. 이미지 지연 로딩 기법을 사용할 때
    • 로딩되지 않은 이미지의 높이가 0으로 계산된다.
    • 높이를 기준으로 잡혀진 레이아웃은 깨질 수 있다.
  4. content-visiblity: auto 속성을 사용할 때,

요소와 문서의 전체 높이를 일정하게 유지하는데에 필요.

수진 마진 병합

인접된 형제나 부모 자식 사이에서의 수직마진이 병합된다.

  1. 블럭요소 사이에서만 발생한다.
  2. 양수끼리, 음수끼리 만난 경우 절대값이 큰 값이 적용된다.
  3. 양수와 음수가 만난 경우 두 값의 합이 적용된다.

이유

초기 스타일에 수직 마진이 있는 요소로 인해 양쪽 수진 마진에 발생하는 과도한 간격을 상쇄하기 위해 발생한다.

예외

  • 최상위 요소(body)의 수직 마진.
  • 부모의 display:flow-root
  • 부모의 overflow:hidden|auto|scroll
  • 부모의 padding-top/bottom 값이 0이 아닐 때.
  • 부모의 border-top/bottom 값이 0이 아닐 때
  • display:inline|inline-*
  • float:left|right

레이아웃에 대하여

레이아웃

display와 position등이 있다.

display

레이아웃의 한 요소이다.

changed display

position을 바꾸는 여러 속성들이 있다.

아래의 속성들을 사용할 경우 block:display를 굳이 사용할 필요가 없다.

1
2
3
4
position: absolute|fixed
float: left|right
/* changed display */
display:block

display:inline

요소들을 행으로 배치시키는 속성이다.

수평마진과 수평패딩만 적용할 수 있다.

수직패딩의 경우 다른 요소를 밀어내지는 못한다.

display:block

요소들을 열로 배치시키는 속성이다.

너비와 높이, 수평마진, 수직마진, 수평패딩, 수직패딩 전부 적용할 수 있다.

수직마진은 중첩된다.

display:inline-block

block과 inline-block의 특징을 전부 가지고 있는 속성이다.

수직마진은 중첩되지 않는다.

display:none

어떤 장치도 표시하거나 접근할 수 없다.

display:flow-root

블록 컨테이너가 되며 새로운 블록 형식 문맥이 된다.

  • 포함된 float 요소는 컨테이너 끝에서 clear 된다.
  • 부모 자식요소의 수직 마진을 병합하지 않는다.

display:flex

flex 컨테이너 박스를 생성한다.

  • flex 형식 문맥을 설정한다
  • 포함 아이템을 1차원 기반으로 배치.
  • 격자에 구애 받지않는 배치가 가능하다.

display:grid

grid 컨테이너 박스를 생성한다.

  • grid 형식 문맥을 설정한다
  • 포함 아이템을 2차원 기반으로 배치
  • 격자끼리 병합하는 배치가 가능하다.

position

position: static

배치 기준이 없다. 흐름에 따라 배치된다.

left, right, top, bottom, z-index를 사용할 수 없다.

position:relative

박스의 현재 위치가 배치의 기준으로 배치를 변경할 때에 다른 박스의 흐름을 깨지 않는다.

자식 또는 자손 요소의 absolute의 배치 기준이 된다.

left right top bottom z-index,inset을 사용할 수 있다.

position:absolute

일반적인 흐름에서 완전히 이탈한다.

부모 형제의 크기나 위치에 전혀 영향을 미치지 않는다.

조상 박스가 relative,absolute,fixed,transform일 때, 조상 기준으로 배치된다.

left tight top bottom z-index, inset을 사용할 수 있다.

position:fixed

뷰포트가 배치 기준이다.

조상요소에 transform 속성이 있으면 transform 속성이 있는 요소가 배치 기준이다.

left tight top bottom z-index, inset을 사용할 수 있다.

position:sticky

스크롤 포트가 배치 기준이다.

부모 요소가 스크롤 포트에 보이는 동안 스크롤 포트 기준으로 고정된다.

부모 요소가 스크롤 밖으로 이탈하면 고정을 멈춘다.

left tight top bottom z-index, inset을 사용할 수 있다.

z-index

절대값이 아니며 부모요소에 종속된다.

z-index는 부모요소보다 높을 수가 없다.

css 최적화

구글 라이트하우스를 통해 css 최적화에 대한 오류와 기법들이 보고된다.

기본 전제

css 최적화에는 두 가지 기본 전제가 있다.

  1. unused css 제거
  2. render-blocking resources제거

unused css 제거

css는 페이지 렌더링을 차단하는 리소스이기 떄문에 사용되지 않는 css는 브라우저가 스타일을 계산하는데에 잠재적으로 더 많은 시간을 소비하게 만든다.

render-blocking resorces 제거

브라우저가 외부 리소스를 다운로드 하고 파싱하는 동안 페이지 콘텐츠를 파싱하거나 렌더링하지 않기 때문에 페이지 표시 속도 저하의 원인이다.

Unused CSS는 render blocking을 가중하는 요인이다.

render blocking resources

렌더 블로킹 리소스 표시 조건:

  • defer , async 속성이 없는 <head> 요소의 <script>태그
  • mdeia 속성과 값이 없는 <link rel='stylesheet'>태그

script의 여러 속성

  • async
    병렬 다운로드, 즉 스크립트를 다운로드 하면서 웹페이지를 해석한다.
    다운이 끝나면 즉시 실행한다.
  • defer
    병렬 다운로드, 즉 스크립트를 다운로드 하면서 웹페이지를 해석한다.
    그 후 다운로드가 끝나면 웹페이지가 그려지고 DOM이 들어왔을 때 실행한다.

script 개선방안

  1. 필수 스크립트는 head에 <script>형식을 작성한다.
  2. 기타 스크립트는 </body> 종료 태그 직전에 선언한다.
  3. 마지막에 파싱해도 문제가 없다면 defer속성을 사용한다.
  4. 가능한 빠른 시점에 실행이 필요하면 async속성을 사용한다.

css 의 여러 속성

css에 media속성이 없거나 값이 all이면 렌더 차단 리소스이다.

media 속성을 통해 특정 조건에서만 css를 해석하도록 처리하였기 떄문에 render blok resource로 파악되지 않는다.

따라서 반응형 웹 개발시에는 해상도 구간별로 별도의 css를 작성 후 여기에 media 쿼리 구문을 적용하여 개발하면 성능을 개선할 수 있다.

css 개선방안

  • 반응형 웹인 경우 해상도 구간 별로 css 파일을 분리하고 media 속성으로 분기하기
1
2
3
4
5
6
7
<link href="*.css" rel="stylesheet" media=" (max-width:639) " />
<link
href="*.css"
rel="stylesheet"
media=" (max-width:639) and (max-width:960)"
/>
<link href="*.css" rel="stylesheet" media=" (max-width:961) " />
  • 필수 스타일은 페이지의 <head><style> 형식으로 작성하기
1
2
3
<style>
/*필수 스타일*/
</style>
  • 지연 스타일은 <link rel="preload"> 속성으로 병렬 로딩 후 지연 적용하기
    당장 필요하지 않은 css의 경우 병렬로 로딩을 한 후 늦게 화면이 되도록 지연 적용하기

    병렬로 로딩하다가 로딩이 끝나면 onload이벤트를 이용하여 rel을 stylesheet로 적용하여 화면에 적용된다.
    중요하지 않은 CSS 연기

    1
    2
    3
    4
    5
    6
    <link
    rel="preload"
    as="style"
    href="x.css"
    onload="this.onload=null;this.rel='stylesheet'"
    />

LCP(Largest Contentful Paint)

가장 큰 덩어리 콘텐츠를 2.5초 이내에 로딩시켜야 한다.

LCP 개선 사례

  1. 라이브러리 의존도 줄이기
    사용하려는 기능 외에 많은 기능을 포함하고 있기 떄문에 FCP를 늦출 확률이 높다.
    ex)jquery lodash,normalize

    https://youmightnotneed.com을 통해 라이브러리 대신 사용할 수 있는 바닐라 js를 찾을 수 있다.

  2. 사용하지 않는 css 제거
    normalize나 reset과 같이 잘 사용하지 않는 css를 과감하게 버려야한다.

  3. preconnect/preload
    웹 폰트를 preconnect하여 url에 미리 연결하고 바로 다운받을 수 있게 도와준다.
    또한 css를 preload하여 렌더링을 차단하지 않고 css를 로드한다.

    1
    2
    3
    4
    5
    6
    7
    <link rel="preconnect" href="https://fonts.gstatic.com" />
    <link
    rel="preload"
    as="style"
    href="x.css"
    onload="this.onload=null;this.rel='stylesheet'"
    />

LCP를 head에서 preload로 로딩하여 성능을 개선할 수 있다.

  1. feature detection

type과 media라는 조건을 붙여서 최적화한다.

1
2
3
4
5
6
<picture>
<source srcset="small.avif" type="image/avif" media=" (max-width:640px)" />
<source srcset="small.avif" type="image/avif" />
<source srcset="small.webp" type="image/webp" media=" (max-width:640px)" />
<source srcset="small.webp" type="image/webp" />
</picture>
  1. Image Loading / Decoding

loading="lazy"는 뷰포트 안에 들어오지 않으면 로딩을 하지 않는다.

또한 decoding="async"는 화면에 다른 요소를 렌더링하는 걸 중단하지 않고 다른 요소를 먼저 표시하고 이미지를 나중에 표시하는 기법이다.

1
<img src="example.jpg" loading="lazy" decoding="async" />

CLS(Cumulative Layout Shift)

누적 배치 변경이라고 하며 이미 배치가 끝난 컨텐츠의 위치가 바뀌는 상황이다.

0부터 1까지의 범위를 가지고 있으며 0.1 이내로 단축하는 것이 좋다.

CLS 유발 요인

  1. 치수를 알 수 없는 이미지 로딩.
  2. 동적으로 추가된 DOM.
  3. 웹 폰트 swap 페인팅(FOIT/FOUT)

CLS 해결방법

  1. 자리표시자
    동적으로 추가하는 컨텐츠에 들어올 영역에 자리 표시자를 미리 띄워놓고 이미지가 들어오지 않더라도 그 영역을 다른 요소가 침범하지 않도록 막아놓는다.

    • 최소크기를 지정하기

    이미지나 영상이 로딩될 영역의 최소크기를 지정한다.

    1
    2
    3
    4
    .heroBanner {
    min-height: 100px;
    background: silver;
    }
  2. 이미지/ 영상 요소에 비율 힌트 제공

    • img태그에 width와 height값의 비율을 제공하기
      css로 이미지의 크기를 설정할 수 있다고 하더라도 img 태그에 제공 해주는 것이 좋다.
    1
    2
    <img src="..." width="800" height="534" alt />
    <!-- + max-width:100%; height:auto; -->
    • aspect-ratio
      aspect-ratio는 비율을 고정하는 속성인데 아직 많이 지원되지는 않는다.

    • 영상 종횡비 유지하기(padding)
      padding 속성에 %를 주어 부모요소를 기준으로 하게 하여 비율을 유지하게 해준다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .utube {
    position: relative;
    padding-top: 56.25%;
    }
    .utube__iframe {
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    }
  3. 웹폰트 대체 글꼴 지정하기

텍스트 영역의 높이가 늘어나면서 CLS가 발생한다.
비슷한 특징을 갖는 대체 글꼴을 사용하여 바뀌지 않도록 해야한다.

1
2
3
* {
font-family: "Noto Sans KR", Verdana, sans-serif;
}
  1. 애니메이션 적용 시 transform 사용.

reset,normalize

대부분의 초기화 스타일은 쓸모 없거나 덮어쓰는 코드가 대부분이다.

이는 unusedcss를 증가시킨다.

reset.css 최적화하기

1
2
3
4
5
6
7
8
9
10
11
12
body {
margin: 0;
overflow-wrap: break-word;
}
:lang(ko) {
word-break: keep-all;
}

img {
max-width: 100%;
height: auto;
}

클래스 속성이 들어간 요소들에만 reset.css를 적용한다.

링크

1
2
3
[class]{
...
}

선택자 최적화

선택자를 많이 중첩시키지 않는 것이 중요하다.

웹 전반에 대한 내용

패스트캠퍼트 the red를 듣고 정리하였다.

트래픽 과다

서버의 사용량이 많아지면 느려지다가 어느 시점에 서버가 죽는다.

클라이언트 개발자도 서버가 죽었을 경우 대처해야한다.

때문에 모니터링이 필요하다.(status check,health monitoring)

Scaling

서버의 크기를 늘렸다 줄이는 기법( 가능하도록 서버를 설계해야 한다. )
없을경우 대기열을 만들어 한 번에 접속 가능한 유저의 접속을 막아야한다.
클라우드 서버를 사용할경우 용이하다.

리소스 최적화

서버에서 리소스를 내려주면서 발생하는 트래픽의 줄이는데에 중요하다.

리소스를 주고받으며 db,api 그리고 웹사이에 오가는 트래픽을 줄이는 것이다.

파일 크기, 캐싱 정책에 따라 최적화 할 수 있다.

브라우저의 웹 페이지 로드 과정

Prompt for unload : 다른 페이지로 이동할 것이라는 신호(window의 beforeunload 이벤트가 발생한다.)
– 네트워크 레이어
Redirect(선택) : 리다이렉트가 있다면 실행한다.
AppCache : 브라우저 캐시에 저장된 이벤트가 있는지 확인한다.
DNS : dns에서 도메인 이름을 네트워크 주소로 바꾼다.
TCP : tcp를 연결한다.
Request : html을 요청한다.
Response : html을 받는다.

Processing : html을 파싱하고 렌더링한다. 끝날 때 document.DOMContentLoaded이벤트가 발생한다.

  • DONContentLoaded : html을 다 파싱하고 DOM까지 만들고 화면에 그리기 전에 발생시킨다.(서브 리소스등은 아직 다운로드받지 않은 상태이다.)

Load : 이미지나 css등을 다 다운로드 받은 상태이다. 끝날 때 window.load 이벤트가 발생한다.

  • load 리소스들의 로딩이 완료되면 실행된다.

점진적 향상 vs 우아한 성능 저하

점진적 향상은 기능이 없는 환경에서 출발해서 지원하는 환경이 있으면 기능을 추가하는 방법이다.

1
2
3
<picture>
</img>
</picture>

우아한 성능 저하는 지원하는 환경에서 출발하여 지원하지 않는 환경을 대비하는 방식이다.

1
2
3
<video>
<a>
</video>

브라우저의 랜더링 방식

더 나은 사용자 경험을 위해 아래의 과정은 점진적으로 실행된다.

싱글스레드기 때문에 앞의 단계로 돌아가게 되면 뒷 단계가 멈춘다.

  1. 사이트에서 HTML을 받아온다
  2. HTML을 위에서 아래로 읽으면서 link요소등 외부 리소스를 만나면 다운로드 받고 js파일을 만나면 스크립트 해석이 끝날 때 까지 렌더링을 멈춘다.
  3. css도 해석하고 html도 해석이 끝나면 각각 CSSOM과 DOM을 만든다.
  4. 둘을 조합하여 렌더트리를 만든다.
  5. 렌더트리가 나오게 되면 렌더트리를 바탕으로 요소를 배치할 영역을 정하는 레이아웃을 수행한다.(reflow)
  6. 요소를 실제로 픽셀로 변환하여 paint를 수행한다.
  7. 픽셀로 그려진 여러 레이어를 합성한다.

DOM을 변경했을 경우

배치가 바뀌지 않았을 경우 레이아웃은 생략할 수 있다.
하지만 페인트는 무조건 진행되어야 한다.
레이아웃을 발생시키는 속성을 사용할 경우 60프레임을 유지하기가 어렵다.
때문에 레이아웃을 줄여 성능을 최적화 할 수 있다.

position, left, top, right, bottom 은 레이아웃에 영향을 준다.
하지만 transform:translate()는 레이아웃에 영향을 주지 않아 paint만 발생한다.

reflow는 cpu 자원을 많이 사용하고 repaint는 gpu 자원을 많이 사용한다.

때문에 SPA SSR CSR과 직접적으로 연관성이 있다.

CSR

클라이언트(브라우저)에서 렌더링을 수행한다.
즉 html에 기초적인 것만 적고 js를 이용해 DOM을 그린다.

페이지 이동등이 일어날 때 history api를 이용하여 라우팅 처리를 하고 필요한 DOM만 그 때 그 때 만들어 렌더를 처리한다.

html의 용량이 낮은 대신 js의 용량이 크다.

html의 용량이 작다는 것은 로딩까지의 접근이 빠르다.

또한 필요한 DOM만 만들어서 쓸 수 있다.

리앵뷰 모드 CSR이 베이스이다.

페이지를 이동거나 데이터를 새로 받아오면서 깜빡거림이 발생하지 않는다.

장점

한 번 렌더링에 성공한 경우 그 이후 필요한 부분만 렌더링 할 수 있어 경우에 따라 성능상의 이점이 있다.

단점

js 번들 사이즈가 커진다. 또한 DOM을 브라우저에서 직접 만들기 때문에 브라우저의 리소스를 많이 쓰게되어 렌더링 퍼포먼스가 저하된다.

특징

뉴스 기사나 아티클 위주의 사이트들은 CSR을 할 이유가 없다.

하지만 페이스북이나 인스타그램, 트위터 등 실시간성이 유지될 수록 좋은 어플리케이션의 경우 실시간으로 데이터를 받아오고 처리할 수 있어 CSR을 사용하면 좋다.

SSR

서버에서 렌더링을 수행한다.
즉 서버에서 api를 통해 db를 참조하여 데이터를 받아와서 html을 그리는 모든 과정을 서버에서 수행한다.

장점

서버가 브라우저보다 성능이 좋아서 CSR보다 빠르다.

HTML이 이미 있는 상태로 내려오기 때문에 콘텐츠가 렌더링 되는 속도가 빠르고 검색엔진이 체감하는 속도도 빠르다.

검색엔진은 빠르게 뜨는 사이트를 우선적으로 노출시키기 때문에 콘텐츠가 뜨는 속도가 빨라서 SEO에 이롭다.(상위노출되기 쉽다.)

단점

서버에서 해야할 작업이 많다.

CSR에 비해 최초 로딩속도는 느리다.

특징

html을 만들어 서버와 클라이언트와 주고받는 시간이 늘어날 수록 ux가 안 좋아진다.

콘텐츠가 많을 경우 SSR을 사용한다.

next js나 nodejs에서 template engine을 통해 구현한다.

SSG

사이트를 생성(빌드)하는 시점에 html을 생성한다.

html을 다 만들어놓고 s3나 cdn에 올려서 유저에게 그대로 전달한다.

장점

사이트 생성할 때만 api에 접근하기 때문에 api서버에 대한 부하가 훨씬 줄어든다.

대용량 트래픽에 굉장히 최적화되어있다.(어지간하면 안 죽는다. cdn이 죽어야 죽는다.)

단점

실시간 사이트에는 어울리지 않는다.

특징

데이터가 자주 갱신되지 않는 사이트에 대해서는 최고의 방법이다.

nextjs는 SSG과 SSR을 전부 지원한다.

상세페이지를 만들 경우 SSG 실시간페이지를 만들경우 SSR으로 만들 수 있다.

getsby는 SSG에 최적화되어있다.

정리

SSR,CSR,SSG를 적절히 섞어서 사용하는 것이 좋다.

예를들어 장바구니는 실시간성이 제일 중요하다. 때문에 csr이 어울린다.

상품상세의 경우 데이터가 자주 바뀌지 않는다. 때문에 금액,옵션 그리고 품절은 빠르게 바뀌어야 한다.

때문에 상품상세는 ssr이나 ssg로 만들어도 금액이나 옵션등은 CSR로 구현하는 것이 좋다.

늦게 업데이트 되어도 크게 문제가 되지 않는 블로그나 기사같은 경우 ssg로 구현하는 것이 좋다.

Why React?

  1. 강력한 커뮤니티

리액트는 페이스북이 지원하기 떄문에 망하지는 않을 것이라는 확신과 개발 커뮤니티들도 제일 크다.

  1. 확장성

리액트를 사용한 써드파티 라이브러리들이 굉장히 많다.

  1. 리액트 네이티브

개념은 조금 다르지만 확장할 수 있다.

  1. 경쟁상대의 부재

리액트는 ui자체에 포커싱이 되어있는 라이브러리이다.

Why Function Component

객체지향의 관점에서는 필요하지만 ELEMENT를 만드는 관점에서는 불필요한 작업들이 많이 요구된다.

함수형 컴포넌트가 더 직관적이고 불필요한 작업이 없다.

클래스형 컴포넌트의 경우 라이프사이클로 인해 사용하였다.

HOOKS가 나와서 이 라이프사이클을 대체하게 되었다.

무엇보다 순수함수인 함수형 컴포넌트가 클래스형보다 좀 더 빠르다.

또한 커스텀 HOOKS를 통해 재활용할 수 있다.

FE 성능 최적화

패스트캠퍼스 the red를 듣고 정리해보았다.

파일 다운로드

최신 브라우저는 대체로 도메인 당 6개의 접속만 동시에 처리한다(HTTP 프로토콜 /1.1기준)
http2에서는 하나의 연결로 계속 파일을 주고받아서 괜찮다.
하지만 아직은 1.1기준으로 작동하는 사이트가 많다.

한 도메인에서 6개의 파일만 동시에 다운로드 할 수 있다.
6개 이상부터는 큐에 넣어두고 하나씩 다운로드 하게 된다.

이미지와 같은 파일들은 사용자가 기다리는것에 익숙하지만
js나 cs파일들의 다운로드가 느려지면 페이지에 변화가 없고 인터액션이 늦어지게 된다.

개선방안

  1. 파일 갯수와 용량을 작게한다.

  2. 별도 도메인이나 CDN등으로 분산하여 해결할 수 있다.

  3. 적절한 포맷을 사용한다. 이미지의 경우 webp파일을 사용하여 크기를 줄일 수 있다.
    ex) 레진코믹스

1
2
3
4
5
6
<!-- 지원 타입에 따라 다른 이미지 로딩 -->
<!-- img 태그를 picture태그로 감싸서 picture태그를 지원하는 브라우저에서는 picture를 해석하도록 만들었다. -->
<picture>
<source srcset="img/photo.webp" type="image/webp" />
<img src="img/photo.jpg" alt="my photo" />
</picture>
  1. 화면 크기와 해상도에 따라 적절한 이미지 로딩
1
2
3
4
5
6
7
8
<!-- 화면 크기에 따라 적절한 이미지 로딩 -->
<picture>
<source srcset="img/photo_small.jpg" media="(max-width:800px)" />
<img src="img/photo.jpg" alt="my photo" />
</picture>

<!-- 해상도에 따라 적절한 이미지 로딩 -->
<img src="img/icon72.png" alt="icon" srcset="img/icon14.png 2x" />
  1. 웹 폰트 최적화
  • 구글폰트: 나눔 고딕 등 한국어 폰트 26종 제공
  • WOFF2는 WOFF,TTF등에 비해 30% 용량이 작다.
  • 필요한 글자만 골라서 글꼴을 만들 수도 있다.(메뉴용 글꼴 등)
  1. 화면 크기 등에 따라 필요한 스타일 시트만 로딩
  • 하지만 실제 이렇게 사용하는 경우는 없다. css는 크기가 작고 파일 개수를 늘리는 것은 좋지 않다.
  • 부트스트랩이 차지하는 용량이 20kb쯤
1
2
3
4
5
6
<link href="mobile.css" rel="stylesheet" media="all" />
<link
href="desktop.css"
rel="stylesheet"
media="screen and (min-width: 600px)"
/>
  1. link 태그 만으로 접속 시간 절약
1
2
3
<link rel="dns-prefetch" media="https://taegon.kim" />
<link rel="preconnect" media="https://cdn.example.com" />
<link rel="prefetch" media="https://cdn.example.com/library.min.js" />

Preconnect를 사용할 경우 외부 도메인을 미리 검색하여 소켓을 연결해둔다. 서드파티 자원 연결에 적합하다.

Prefetch를 사용할 경우 외부 도메인을 미리 검색하여 소켓을 연결 후 다운로드까지 해둔다.
하지만 남용하면 안되고 꼭 필요하거나 다음 경로가 명확하게 예상이 될 경우에만 사용해야 한다.

로딩 속도 개선

  1. 필수 컨텐츠가 아니라면 비동기 로딩을 고려해보자(광고,댓글,헤더/푸터 등)

  2. 이미지/아이프레임/스크립트 등은 필요할 때까지는 읽지 않는 게으른 로딩(lazy loading)기법을 고려해보자. 코드를 분할한 후 필요할 때 읽어오는 방식이다. 웹팩에서 자동으로 해주기도 한다.

1
2
<img src="image.jpg" loading="lazy" />
<iframe src="https://example.com" loading="lazy"></iframe>
  1. 시간이 많이 걸린다면 플레이스 홀더 등으로 대체한다.

-스켈레톤 ui

계산 시간

  1. 웹 워커

자바스크립트는 싱글스레드이다. 하지만 워커스레드를 이용하여 멀티스레드가 가능하다.

워커스레드

  • UI를 조작할 수 없다.
  • 워커 스레드 전용으로 분리된 파일이 필요하다.
  • postMessage()로 데이터를 전송하고, onMessage 이벤트를 통해 받는 방식으로 통신한다.
  1. 느긋한 계산

값이 필요해질 때 까지 계산을 미뤄두는 기법이다.

게으른 평가, 지연 평가라고도 한다.

  1. 메모이제이션

계산 결과를 기억해두고 반복 사용하는 기법.
루프, 재귀 호출 등 최적화.

반응시간

지나치게 느린 애니메이션 및 응답 속도는 사용자 경험을 저해한다.

  1. 시스템의응답이 40ms보다 느리면 주의력 분산이 발생함
  2. ui가 100ms 이하로 반응해야 ui를 다루고 있다고 느낀다.
  3. 애니메이션은 60fps를 기준으로 한다.

애니메이션의 렌더링 순서

js-> 스타일-> 레이아웃 ->페인트 -> 합성

페인트까지는 메인스레드에서 진행한다. 합성만 컴포지터 스레드에서 진행한다.

메인스레드는 자바스크립트가 동작하기 때문에 메인스레드의 작업이 많아지면 자바스크립트의 속도도 느려진다.

개선 방법

메인스레드에서 이루어지는 과정은 가능한 한 피하고 컴포지터 스레드에서 일어나도록 하면 된다.

즉 레이아웃이나 페인트는 피하고 합성단계를 유발하는 css 속성들을 주로 사용한다.

각 단계의 동작을 유발하는 css 속성은 https://csstriggers.com/에서 확인할 수 있다.

정리

  1. 가능하면 js보다는 css 애니메이션을 최대한 활용한다.
  2. 다음 css 속성 위주로 애니메이션 -gpu 가속을 적용한다.(자동으로 적용된다.)
  • transform:translate or scale or rotate
  • opacity
  1. 레이아웃 변경이나 리페인팅을 유발하는 css 속성은 비용이 많이 든다.
  • 레이아웃 : width,height,padding, margin,display
  • 페인트 : color,background,outline,box-shadow
  1. setTimeOut 보다는 requestAnimationFrame, Web Animations API 등 활용
  • requestAnimationFrame: 브라우저가 최적화, 비활성 탭에서는 동작 안 함
  • web Animation API: css애니메이션과 같은 애니메이션 엔진 사용
  1. three.js velocity.js와 같은 고성능 애니메이션 라이브러리 사용

  2. will-change 속성을 통해 브라우저가 최적화 할 속성 명시

  • 남용하면 리소스 낭비,
  1. DOM 접근과 업데이트는 가능한 적게, 한 번에 몰아서 처리,
  • 엘리먼트 추가는 DocumentFragment 활용
1
2
3
4
5
6
7
8
9
10
const app = document.getElementById("app");
const frag = document.createDocumentFragment();

for (let i = 0; i < 1001; i++) {
const el = document.createElement("div");
el.innerText = `Element ${i}`;
frag.appendChild(el);
}

app.appendChild(frag);

리소스

cpu 점유율, 전력 소비량, 스토리지, 메모리 사용량, 네트워크 트래픽 중 메모리 누수와 네트워크 트래픽을 신경써야 한다.

메모리 누수

프로그램이 필요하지 않는 메모리를 계속 점유하는 현상

메모리 생명 주기

  1. 할당 : 사용할 메모리를 확보한다.
  2. 사용 : 메모리를 사용한다.
  3. 헤재 : 불필요한 메모리를 반환한다.

개선 방법

js는 가비지 컬렉션을 통해 메모리를 해제한다.

이 메모리를 몇개의 변수가 참조하는지 카운트하여 0개일 경우 지운다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var x = {
a: {
b: 2,
},
};
// 두 객체 모두 생성된다.
// x 객체를 o1 a객체를 o2라고 하자

var y = x; //y도 o1를 참조한다.

x = 1; // x는 이제 o1를 참조하지 않는다. 이제 o1를 참조하는 변수는 y가 유일하다.

var z = y.a; // o1 내부의 o2을 z가 참조한다.

y = "mozilla"; // y는 o1를 참조하지 않지만 z변수가 o1의 o2를 참조하므로 o1은 제거되지 않는다.

z = null; // 이제 o1과 o2는 아무 곳에서도 참조가 되지않아 가비지 컬렉션이 된다.

순환참조 문제

객체끼리 참조가 맞물려서 가비지 컬렉션이 동작하지 않는 문제

마크스위프 알고리즘을 통해 해결할 수 있게 되었다.

메모리 누수의 대표적 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// case 1 : 큰 전역 변수는 계속 메모리에 존재한다.
const bigData={.../*big data*/};

// case 2 : 클로저는 생성된 실행 컨텍스트의 변수를 해제하지 못하게 한다.
function factory(){
const largeData={.../*big data*/}

return ()=>{
//...
}
}

const fn=factory();
fn();
// fn을 실행한 후에도 여전히 largeData는 메모리에 존재한다.

// case 3: 큰 데이터가 다른 변수에서 참조되면 큰 데이터는 해제되지 않는다.
const data1={linkTo:bigData};
const data2={anotherName:bigData};

개선방안

아래의 코드는 예시일 뿐이다.

실제로는 모듈이나 다른 함수로 분리해서 사용하는 것이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// case 1 : 큰 전역 변수는 계속 메모리에 존재한다.
const bigData={.../*big data*/};

// case 2 : 클로저는 생성된 실행 컨텍스트의 변수를 해제하지 못하게 한다.
function factory(){
const largeData={.../*big data*/}

return ()=>{
//...
}
};

{
const fn=factory();
fn();
}
// fn은 실행 후 현재의 Lexical Enviroment와 함께 해제되고 largeData도 함께 해제된다.

// case 3: 큰 데이터가 다른 변수에서 참조되면 큰 데이터는 해제되지 않는다.
const data1={linkTo:bigData};
const data2={anotherName:bigData};

네트워크 트래픽

파일 용량을 줄이거나 필요할 때만 불러와서 트래픽 낭비를 줄인다.

  1. 최소화된 js,css파일을 사용한다.
  2. 프레임워크는 한 개 이하만 사용한다.
  3. 파일 주소의 파라미터는 주의해서 사용, 의도하지 않은 캐시버스터가 될 수 있다.
  • 브라우저는 전체 url 기반으로 캐시하기 때문에 업데이트가 되어도 받아오지 못할 수 있다.
  • 일부러 랜덤한 숫자를 붙여 새로 캐시되도록 하는 것이 캐시버스터이다.
  1. 이미지, 미디어는 필요할 때 불러오는 게으른 로딩(lazy loading)기법을 사용한다.

d3 + react tooltip 만들기

마우스를 호버했을 때 뜨는 툴팁을 만들기 위한 두 가지 방법이 있다.

overlay를 사용하기

첫 번째 방법은 svg 내부에 많은 요소들이 있어서 요소를 어디에 등록할지 애매할 경우에 유용하다.

필수적인 조건은 요소들이 Scale로 위치를 계산하여 svg 내부에 위치해야 한다.

첫 번째 방법은 요소 최상단에 보이지 않는 얇은 층을 쌓는 것이다.

나는 임의로 overlay라고 이름지었다..!

opacity를 0으로 주어 보이지 않지만 분명히 존재한다.

1
2
3
4
5
6
7
const overlay = svg
.selectAll(".overlay")
.attr("opacity", 0)
.attr("width", width - marginLeft)
.attr("height", height - marginBottom)
.attr("fill", overlayColor)
.style("pointer-events", "all");

이제 이 overlay에 이벤트를 등록하면 된다.

1
2
3
4
5
6
7
8
9
10
11
overlay
.on("mouseover", focus.style("display", null))
.on("mouseout", focus.style("display", "none"));
.on("mousemove", function(e){
const focus = select(".focus");
const bisectDate = bisector((data) => data.date).left;
const curPosition = xScale.invert(pointer(e, e.target)[0]);
// 현재 포인터를 기반으로 xScale 내부에서의 위치정보를 구한다.
const i = bisectDate(data, curPosition, 1);
// 현재 마우스 위치를 기반으로 데이터 배열에서 해당 위치의 index를 구한다.
})

마우스 위치에 가까운 요소에 직접 접근할 수 있다.

요소에 직접 이벤트 등록하기

body에 relative속성을 주고 focus에 position:absolute를 주어 left와 right로 위치를 정해주는 방법이다.

어떤 요소를 기준으로 하냐에 따라 eventX를 사용할지 offsetX를 사용할지가 달라진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var tooltip = d3
.select("#my_dataviz")
.append("div")
.style("position", "absolute")
.style("visibility", "hidden")
.text("I'm a circle!");

d3.select("#circleBasicTooltip")
.on("mouseover", function () {
return tooltip.style("visibility", "visible");
})
.on("mousemove", function (event) {
return tooltip
.style("top", event.pageY - 800 + "px")
.style("left", event.pageX - 800 + "px");
})
.on("mouseout", function () {
return tooltip.style("visibility", "hidden");
});

d3 + react 유용한 함수

getTextWidth

text의 width를 반환하는 함수이다. d3로 차트를 조작할 때 텍스트의 너비보다 텍스트를 포함하는 요소의 너비가 작을경우의 예외처리를 할 수 있다.

1
2
3
4
5
6
export default function getTextWidth(text, fontSize, fontFace) {
var canvas = document.createElement("canvas");
var canvasContext = canvas.getContext("2d");
canvasContext.font = fontSize + "px " + fontFace;
return canvasContext.measureText(text).width;
}

isBright

16진수 색상코드를 받아서 밝은 색상인지 어두운 색상인지를 반환하는 함수이다.

배경색에 따른 글자 색상 처리를 해줄 때에 유용하게 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
export default function isBright(rgbCode) {
let c = rgbCode.substring(1);
let rgb = parseInt(c, 16);
let r = (rgb >> 16) & 0xff;
let g = (rgb >> 8) & 0xff;
let b = (rgb >> 0) & 0xff;

let luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;

return luma > 127 ? true : false;
// true일경우 밝음, 아닐 경우 어두움
}

d3 + react 유용한 hooks

d3와 react를 사용할 때에 유용하게 쓰일 수 있는 몇 가지 custom hooks이 있다.

useResizeObserver

d3로 만든 차트가 반응형으로 작동하도록 하는 hooks이다.

해당 html요소를 관찰하다 크기변화가 발생하면 내부의 callback을 실행한다.

svg의 width가 100%여야 정상적으로 작동한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useEffect, useState } from "react";
import ResizeObserver from "resize-observer-polyfill";

export const useResizeObserver = (ref) => {
const [dimensions, setDimensions] = useState(null);
useEffect(() => {
const observeTarget = ref.current;
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
setDimensions(entry.contentRect);
});
});
resizeObserver.observe(observeTarget);
return () => {
resizeObserver.unobserve(observeTarget);
};
}, [ref]);
return dimensions;
};

export default useResizeObserver;

usePrevValue

이전 렌더링의 값을 저장하는 hooks이다.

useEffect 내부에서 조건부 작업을 할 경우에 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useEffect, useRef } from "react";

function usePrevValue(value) {
const prevRef = useRef(null);

useEffect(() => {
prevRef.current = value;
});

return prevRef.current;
}

export default usePrevValue;

useDebounce

d3에는 수많은 이벤트가 발생한다!

zoom 이벤트나 위에서 언급한 useResizeObserver의 resize이벤트등의 요청을 줄일 수 있는 hook이다.

delay 안에 새로운 이벤트가 발생하면 기존의 값을 리턴한다.

마지막 요청 이후 delay만큼의 시간이 지나면 새롭게 세팅된 값을 리턴한다.

즉 디바운스를 hook으로 만들었다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useEffect, useState } from "react";

export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};

export default useDebounce;

적용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const App = ({ data }) => {
const containerRef = useRef();
const wrapperRef = useRef();
const dimensions = useResizeObserver(wrapperRef);
const resize = useDebounce(dimensions, 200);

useEffect(() => {
const svg = select(svgRef.curent);
const container = select(containerRef.current);

if (!resize) return;

const { width, height } = resize;
svg.attr("width", width).attr("height", height);
}, [data, resize]);

return (
<div ref={containerRef}>
<svg ref={svgRef} />
</div>
);
};