제어 컴포넌트와 비제어 컴포넌트

리액트의 제어컴포넌트와 비제어 컴포넌트의 개념에 대해 알아보자

제어 컴포넌트

제어 컴포넌트는 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트 하는 컴포넌트이다.

state를 신뢰 가능한 단일 출처로 두고 state에 의해 컴포넌트가 제어된다.

사용자의 입력으로 state를 관리하기 때문에 사용자의 입력값과 state가 실시간으로 동기화된다.

장점

state를 신뢰가능한 단일 출처로 둔다.

즉 사용자의 입력에 따라 state를 변화시키고 state에 의해 업데이트되기 때문에 안정적이다.

때문에 데이터의 유효성을 다룰때에도 훨씬 좋다.

단점

사용자가 입력하는 모든 데이터들에 의해 동기화된다.

가령 <input/>에 text를 제공할 경우 매 타자마자 동기화된다.

=> 이는 쓰로틀링이나 디바운싱을 통해 해결할 수 있다.

비제어 컴포넌트

비제어 컴포넌트는 state로 관리하는게 아닌 DOM 자체에서 데이터를 관리하는 컴포넌트이다.

보통 state가 아닌 useRef를 관리하여 DOM 엘리먼트의 data를 참조한다.

즉 DOM을 신뢰 가능한 출처로 둔다.

useRef는 매 렌더링마다 동일한 객체를 제공하기 때문에 얕은비교 연산에서 항상 true를 반환한다.

장점

<input/>에 text를 제공할 경우를 생각해보면 매 타자마다 리렌더링 되지 않는다. 때문에 불필요한 렌더링을 줄이는데에 도움이 될 수 있다.

이 방식을 사용하는 대표적인 라이브러리는 react-hook-form이다.

Ref

제어 컴포넌트 (Controlled Component)
비제어 컴포넌트
React: 제어 컴포넌트와 비제어 컴포넌트의 차이점

force layout

js에서 요청/호출되는 일부 속성은 브라우저가 스타일과 레이아웃을 동기적으로 계산하도록 한다.

layout trashing

웹 브라우저는 레이아웃 변경을 즉시 처리하지 않고 비슷한 Style의 수정을 모아서 하게 된다. 즉 화면에서 레이아웃에 대한 정보를 실제로 알아야 할 때까지 레이아웃 계산을 지연시킨다.

layout trashing이 발생하는 경우

DOM이 변경되지 않았음을 보장할 수 있는 경우엔 레이아웃 캐시(이전 계산된 값)에서 값을 가져온다.

하지만 offsetHeight과 같은 최신으로 동기화된 레이아웃에 대한 정보에 접근하는 속성같은 경우 강제로 레이아웃 계산(리플로우)를 발생시켜 동기화 후 값을 가져오게 된다.
즉 아래와 같이 코드를 작성하게 되면 불필요한 레이아웃 계산을 과정이 요구된다.

1
2
3
4
elementA.className = "a-style";
var heightA = elementA.offsetHeight; // layout is needed
elementB.className = "b-style"; // invalidates the layout
var heightB = elementB.offsetHeight; // layout is needed again

layout trashing이 발생하지 않는 경우

위 코드는 다음과 같이 수정할 수 있다.

1
2
3
4
elementA.className = "a-style";
elementB.className = "b-style";
var heightA = elementA.offsetHeight; // layout is needed and calculated
var heightB = elementB.offsetHeight; // layout is up-to-date (no work)

똑같은 작업을 하는 코드지만 위의 코드에 비해 레이아웃 계산의 수가 줄어들게 된다.
레이아웃 계산을 강제로 발생시키는 속성은 여기에서 확인할 수 있다.

연속된 layout trashing 최적화하기

paragraph의 너비를 box의 너비와 같도록 하는 코드를 작성한다고 생각해보자

1
2
3
4
5
6
function resizeAllParagraphsToMatchBlockWidth() {
// Puts the browser into a read-write-read-write cycle.
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + "px";
}
}

위와같이 작성한다면 box의 offsetWidth에 접근한 뒤 paragraphs[i]의 너비로 설정하게 된다.

offsetWidth 속성에 접근할 때마다 강제로 레이아웃 계산을 시도하게 되므로 단일 paragraph마다 layout 계산이 일어나게 된다.

이는 다음과 같이 수정할 수 있다.

1
2
3
4
5
6
7
function resizeAllParagraphsToMatchBlockWidth() {
let width = box.offsetWidth;
for (var i = 0; i < paragraphs.length; i++) {
// Now write.
paragraphs[i].style.width = width + "px";
}
}

reflow를 강제로 발생시켜 애니메이션 실행하기

브라우저는 비슷한 style변화를 모아서 반영하기 때문에 원하는대로 애니메이션이 작동하지 않을 때가 있다. 이럴 때는 강제로 reflow를 발생시켜 해결할 수 있다.

1
2
3
4
5
6
7
8
<h2>no force reflow</h2>
<div class="wrapper">
<div class="item"></div>
</div>
<h2>force reflow</h2>
<div class="wrapper">
<div class="item2"></div>
</div>

아래는 css이다. 애니메이션을 해제하는 클래스를 정의하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.wrapper {
width: 500px;
height: 50px;
position: relative;
border: 1px solid black;
}
.item,
.item2 {
height: 100%;
width: 50px;
background: red;
position: absolute;
left: 0;
-webkit-transition: left 500ms linear;
transition: left 500ms linear;
}
.notransition {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
transition: none !important;
}

제대로 작동되지 않는 경우

브라우저의 작동원리로 인해 애니메이션이 제대로 작동하지 않는 경우이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
const $wrapper = document.querySelector(".wrapper");
const $item = document.querySelector(".item");
const $item2 = document.querySelector(".item2");

$item.addEventListener("click", function () {
const itemWidth = $item.clientWidth;
const wrapperWidth = $wrapper.clientWidth;
const itemRightLimit = wrapperWidth - itemWidth;
$item.classList.add("notransition");
$item.style.left = `${itemRightLimit}px`;
$item.classList.remove("notransition");
$item.style.left = "50px";
});

코드만 보았을 때 item의 애니메이션 동작은 다음과 같은 순서로 일어나야 한다.

  1. 애니메이션 해제(notransition 추가)
  2. 박스가 부모박스 오른쪽 끝에 붙도록 left 수정
  3. 애니메이션 설정(notransition 제거)
  4. left50px로 수정

실제로는 다음과 같이 동작한다.

  1. Reflow(속성에 접근했기 때문)
  2. style 수정을 통합함
  3. 애니메이션 설정(notransition 제거)
  4. left50px로 수정(통합된 변경사항)
  5. Repaint

제대로 작동하는 경우

reflow를 강제로 발생시켜 제대로 작동하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
//...
$item2.addEventListener("click", function () {
const item2Width = $item2.clientWidth;
const wrapperWidth = $wrapper.clientWidth;
const itemRightLimit = wrapperWidth - item2Width;
$item2.classList.add("notransition");
$item2.style.left = `${itemRightLimit}px`;
$item2.clientLeft;
$item2.classList.remove("notransition");
$item2.style.left = "50px";
});

의도한 애니메이션은 item과 같다.

그리고 의도한대로 작동한다. item2의 동작 순서는 다음과 같다.

  1. Reflow
  2. 애니메이션 해제(notransition 추가)
  3. 빨간 박스가 부모박스 오른쪽 끝에 붙도록 left 수정
  4. Repaint
  5. Reflow
  6. 애니메이션 설정(notransition 제거)
  7. left50px로 수정
  8. Repaint

4와 5의 Repaint와 Reflow는 clientLeft속성을 조회하여 브라우저를 강제로 동기화했기 때문에 일어난다.

실제 작동하는 예시는 여기에서 예제를 확인할 수 있다.

Ref

What forces layout / reflow

WHAT IS A FORCED REFLOW AND HOW TO SOLVE IT?

Javascript로 애니메이션 제어하기 (Force Reflow)

FOUC와 해결방안

FOUC는 외부의 css를 불러오기 전에 스타일이 적용되지 않은 웹페이지가 나타나는 현상이다.
즉 브라우저의 CRP에서 Render Tree가 노출된 후 css와 js로 인해 DOM이 변경되면 변경사항을 적용하기 전 화면이 노출되는 현상이다.
IE에서 주로 발생한다.(IE11에서도 여전히 발생한다고 한다.)

@Import를 사용한 css

IE를 제외한 브라우저는 @import된 스타일이 적용될 때까지 화면에 표시하지 않는다.
하지만 IE는 화면에 노출된 상태로 스타일을 적용하여 FOUC를 유발한다.

다음과 같은 해결방법이 있다.

Preload 사용하기

preload 속성을 사용하여 중요한 css를 렌더링 이전에 사용할 수 있도록 한다.

css 링크 옮기기

head 요소안에 css를 링크하는 방식으로도 해결할 수 있다.

렌더링 지연시키기

render block 요소인 <link rel="stylesheet">을 사용하여 렌더링을 고의로 지연시켜 해결할 수 있다.

Proper caching 적용하기

  • 브라우저 캐시 혹은 cdn을 사용하여 정적 리소스를 받아오는 시간을 줄인다.

웹 폰트의 사용

@import를 사용하여 스타일링을 할 때와 같은 원리로 FOUC가 발생한다. IE는 웹폰트를 사용할 경우 기본 폰트를 불러들여 화면에 노출시키고 이후 사용된 웹 폰트로 다시 변경하기 때문에 발생한다.

다음과 같은 해결방법이 있다.

물론 위에서 언급한 preload와 같은 해결방법도 유효하다.

Font Loading API 사용하기

Font Face Observer 라이브러리는 웹 폰트의 로딩 상태를 추적할 수 있는 폰트 로더로, 파일 크기가 작고 실행속도가 빠르다는 장점이 있다.

다음과 같이 사용할 수 있다.

웹 폰트가 적용되지 않은 상태와 적용한 상태의 css를 적어둔다. 그 후 적용되지 않은 상태의 css가 먼저 적용되도록 한다.

1
2
3
4
5
6
7
body {
font-family: "Apple SD Gothic Neo", sans-serif;
}

body.fonts-loaded {
font-family: "Roboto", "Apple SD Gothic Neo" sans-serif;
}

그 후 사용할 웹 폰트로 FontFaceObserver의 인스턴스를 만들고 load 이벤트를 등록한다.

1
2
3
4
5
let font = newFontFaceObserver("Roboto");

font.load().then(function () {
document.body.classList.add("fonts-loaded");
});

font-display 속성

font-display 속성을 사용하여 웹 폰트의 로딩 상태에 따른 동작을 설정할 수 있다.
swap 속성을 통해 fallback font를 사용할 수 있다.

fallback font로 글자를 렌더링하고 로딩이 완료되면 웹 폰트를 적용한다.

1
font-display: swap;

SSR

SSR의 경우 SSR 단계에서 스타일을 해주어 해결할 수 있다.

공식문서나 바벨 참조하기,,^^

요약

웹폰트 - preload하여 사전에 받아와라, 폴백 폰트를 준비해라
css - 폰트보단 우선순위가 떨어지지만 preload해라. 안 될경우 head에서 받아와라.
최후의 방법으로는 렌더링을 지연시켜라..

Ref

FOUC(Flash of Unstyled Content)
웹 폰트 사용과 최적화의 최근 동향

css in js는 정답일까?

최근의 컴포넌트 형식의 개발방법에 비해 css는 독립적으로 구성할 수 없다.

css를 포함하는 StyleSheet는 js를 포함하는 script와는 상관없이 과정이 진행되기 때문이다.

CSS in JS

CSS in JS 방식의 라이브러리는 js를 사용하여 컴포넌트 단위로 스코프를 나누었다.

js 변수로 선언된 css를 실제 stylesheet로 만들고 랜덤 문자열로 이루어진 클래스명을 만들어 컴포넌트 끼리 학실하게 분리될 수 있게 한다.
(@emotion/css와 같은 css in js 라이브러리를 다루면서 props를 통해 css를 변경할 때 html의 head 태그 안에 새로운 요소들이 추가되는 것을 볼 수 있다.)

게다가 Sass 문법까지 사용 가능하며 변수 사용이 가능하여 클래스 중첩으로 제어하던 부분도 제어할 수 있다.

문제점

장점이 많아보이지만 다음과 같은 단점이 있다.

Script의 코드 증가

StyleSheet의 Script 변환은 HTML 파싱에 사용되는 Script의 코드가 늘어났다는 것을 의미한다.

브라우저 렌더링이 StyleSheet와 Script로 나누어 병렬처리 되던 것이 오직 Script로 이루어짐에 따라 그만큼 속도가 느려진다.

FOUC (Flash of unstyled content)

또한 CSS가 먼저 제공되어 렌더링시 형태가 잡혀있는 기존 방식에 비해 컴포넌트가 렌더링 되며 형태가 잡히기 때문에 원형의 모습이 잠깐 노출(FOUC)된다. 이는 사용자 경험을 저하시킨다.

해결을 위해 빌드시, 사용하는 style을 뽑아서 StyleSheet를 생성해 올려주는 기능이 또 필요하다.

SSR의 문제가 겹친다면 더욱 해결하기 어렵다.
(과거 nextjs에서 css in JS 방식을 사용하였을 때 해당 문제를 겪은 적이 있다.)

정리

css를 사용할지 css in js를 사용할지는 버그의 가능성과 성능 측면에서 고민해야할 것 같다.

Ref

CSS in JS는 무조건 더 좋을까?

requestAnimationFrame와 passive:true로 스크롤 이벤트 최적화하기

성능 최적화 관련해서 공부하다보면 꼭 있는 내용이 requestAnimationFrame이다.

사용해 본 경험이 없어 사실상 죽은 지식이였는데, 이번 기회에 사용해보려고 공부를 해보니까 주로 스크롤 이벤트 최적화에 많이 사용한다고 한다.

스크롤 이벤트 최적화를 위해선 EventTarget.addEventListener()의 pasive 옵션과 requestAnimationFrame에 대한 이해가 필요하다.

passive

EventTarget.addEventListener() 함수의 세 번째 parameter인 options 객체에는 passive라는 옵션이 존재한다.

이는 이벤트 핸들러 내부에서 절대 preventDefault()를 호출하지 않을 것을 나타내는 boolean 값이다.

EventTarget.addEventListener()를 통해 등록된 이벤트는 컴포지터 스레드가 받는다. 이벤트가 발생하면 컴포지터 스레드는 메인스레드에 이벤트를 넘기고 reflow 또는 repaint가 발생하고 렌더링 파이프라인에 따라 리렌더링 되는 것이 일반적인 과정이다.

하지만 passive 값을 true로 할 경우 컴포지터 스레드에서 이벤트를 메인스레드에 넘기고 처리를 기다리지 않고 바로 Composite하여 새로운 프레임을 바로 합성하게 된다.

만약 핸들러 내부에 e.preventDefault()가 존재하여 이를 수행할 경우 메인스레드에서 해당 이벤트 발생시의 기본 동작을 막고 이벤트 핸들러를 수행해야 한다. 하지만 passive값을 true로 할 경우 e.preventDefault()가 핸들러 내부에 존재하지 않는다는 것이 보장되기 때문에 메인스레드의 처리를 기다리지 않고 바로 새로운 프레임을 합성할 수 있게 된다.

명시하지 않아도 최신 브라우저에선 문서 레벨 노드인 Window Document Document.bodytouchstart 이벤트와 touchmove에선 passive의 기본 값을 true로 적용하고 있다.

지원하는 브라우저인지 여부는 다음과 같이 확인할 수 있다.(mdn 출처)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 기능 감지 */
let passiveIfSupported = false;

try {
window.addEventListener(
"test",
null,
Object.defineProperty({}, "passive", {
get: function () {
passiveIfSupported = { passive: true };
},
})
);
} catch (err) {}

window.addEventListener(
"scroll",
function (event) {
/* do something */
// event.preventDefault() 사용 불가
},
passiveIfSupported
);

스크롤 이벤트 최적화하기

스크롤 이벤트를 최적화하는 방법에 대해 알아보자

최적화 x

아래는 최적화 없이 작성한 코드이다.

1
window.addEventListener("scroll", () => console.log("scrolled"));

위와 같은 이벤트 핸들러를 등록할 시 console.log가 계속 찍히게 된다.
console.log말고 reflow를 발생시키는 callback을 등록했을 경우 브라우저에는 더 많은 과부하가 발생하게 된다.

최적화 적용하기

아래는 최적화를 적용하여 작성한 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function optimizeHandler(callback) {
if (!callback) return;
let tick = false;
return function () {
if (tick) return;
tick = true;
requestAnimationFrame(() => {
callback();
tick = false;
});
};
}
window.addEventListener(
"scroll",
optimizeHandler(() => console.log("realcall")),
{ passive: true }
);

callback함수를 받아 최적화된 함수를 반환하는 optimizeHandler 함수를 작성하였다.

위의 코드는 다음과 같은 방식으로 작동한다.

  1. 클로저를 이용할 수 있도록 반환되는 함수 바깥에 tick을 선언한다.
  2. 반환되는 함수 내부에선 tick이 true일경우 별도의 작업을 하지 않고 얼리리턴한다.
  3. 반환되는 함수 내부에서 tick이 false일경우 requestAnimation을 호출하여 callback을 수행하고 tick을 true로 바꾼다.

Ref

{ passive:true } 의 진정한 의미

패시브 수신기로 스크롤 성능 향상

스크롤 등의 이벤트 최적화하기
스크롤 이벤트 최적화

웹 접근성!

웹 접근성은 장애인 사용자가 비장애인 사용자와 동등하게 웹사이트의 정보에 접근하고 이용할 수 있도록 보장하는 것이다.

웹 접근성에 대해서 공부했다고 생각했는데, 부족한 점이 많아서 다시 정리하게 됐다!

적절한 대체 텍스트 제공

KWCAG 2.x:
텍스트가 아닌 컨텐츠는 그 의미나 용도를 인식할 수 있도록 대체 텍스트를 제공해야 한다.

장식 목적인 이미지는 대체 텍스트를 제공하지 않아야 한다.

1
2
3
<img src="valid.jpg" alt="장식" />X

<img src="valid.jpg" alt /> O

주변 문맥을 통해 충분히 설명하고 있는 내용 또한 대체 텍스트가 불필요하다.

내용을 중복으로 제공하면 오히려 불편하게 된다.

1
2
<a href="..."> <img src="..." alt /> description </a> O
<a href="..."> <img src="..." alt="description" /> description </a> X

css 배경 이미지에 의미가 폼함된 경우 IR 기법을 사용해야 한다.

1
2
3
4
.ally {
position: absolute;
opacity: 0;
}

반복 영역 건너뛰기

KWCAG 2.x:
콘텐츠의 반복되는 영역은 건너뛸 수 있어야 한다.

메뉴와 같은 영역은 건너뛸 수 있어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@media (max-width: 960px) {
.skipToMain {
display: none;
}
}
@media (min-width: 961px) {
.skipToMain {
display: block;
height: 1px;
margin-bottom: -1px;
overflow: hidden;
text-align: center;
line-height: 48px;
}
.skipToMain:focus {
height: auto;
margin-bottom: 0;
}
}

표의 구성

KWCAG 2.x:
표는 이해하기 쉽게 구성해야 한다.

표에 제목과 소제목(th,caption)을 제공하여서 스크린리더가 읽도록 할 수 있다.

또한 scope 속성을 통해 어떤걸 설명하는지 명시해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<table>
<caption>
대한민국 지역별 인구 통계
</caption>
<thead>
<tr>
<th scope="col">년도</th>
<th scope="col">서울</th>
<th scope="col">대전</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">2021</th>
<td>...</td>
<td>...</td>
</tr>
</tbody>
</table>

레이블 제공

KWCAG 2.x:
사용자 입력에는 대응하는 레이블을 제공해야 한다.

1
2
3
4
<input id="xxx" />
<label for="xxx">아이디</label>

<input aria-label="아이디" />

자막 제공

KWCAG 2.x:
멀티미디어 콘텐츠에는 자막, 대본 또는 수화를 제공해야한다.

1
2
3
4
<video poster="myvideo.png" controls>
<source src="*.mp4" srclang="en" type="video/mp4" />
<track src="*.vtt" kind="captions" srclang="en" label="English" />
</video>

제목 제공

KWCAG 2.x:
페이지,프레임,콘텐츠 블록에는 적절한 제목을 제공해야 한다.

1
2
3
4
5
6
7
<title>XX 홈페이지에 오신 것을 환영합니다!</title> X
<title>특수 문자 제발 쓰지 마요.</title> X
<title>다른 페이지와 중복 없는 고유한 제목</title>

<iframe src="*.html"></iframe> X
<iframe src="*.html" aria-label="공시 자료"></iframe> O
<iframe src="*.html" aria-label="빈 프레임"></iframe> O

정지 기능 제공

KWCAG 2.x:
자동으로 변경되는 컨텐츠는 움직임을 제어할 수 있어야 한다.

정지, 이전, 다음 등의 기능을 제공해야 한다.

키보드 사용 보장

KWCAG 2.x:
모든 기능은 키보드만으로 사용할 수 있어야 한다.

장치독립적 이벤트 핸들러를 사용해야 한다.

1
2
3
4
5
6
onblur
onchange
onclick
onfocus
oninput
onselect

초점 이동

KWCAG 2.x:
키보드에 의한 초점은 논리적으로 이동해야 하며 시각적으로 구별할 수 있어야 한다.

텍스트 컨텐츠의 명도 대비

KWCAG 2.x:
텍스트 컨텐츠와 배경 간의 명도 대비는 4.5대 1 이상이여야 한다.

기본 언어 표시

KWCAG 2.x:
주로 사용하는 언어를 명시해야 한다.

1
<html lang="ko"></html>

오류 정정

KWCAG 2.x:
입력 오류를 정정할 수 있는 방법을 제공해야 한다.

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]{
...
}

선택자 최적화

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