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 } 의 진정한 의미

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

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

requestAnimationFrame 메소드

주기적으로 실행되는 애니메이션을 window.requestAnimationFrame()으로 최적화 해보자

requestAnimationFrame(callback)

브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트가 진행되기 전에 해당 애니메이션을 업데이트 하는 함수를 호출하게 한다. 이 메소드는 리페인트 이전에 실행할 함수를 인자로 받는다.

  • 다음 리페인트에서 그 다음 프레임을 애니메이트 하려면 콜백 루틴이 반드시 스스로 requestAnimationFrame()을 호출해야 한다.

대부분의 브라우저에서 W3C 권장사항에 따라 디스플레이 주사율만큼 호출되게 된다.

callback

파라미터인 callback은 다음 리페인트를 위한 애니메이션을 업데이트 할 때 호출할 함수이다. 콜백함수에는 requestAnimationFrame()이 콜백함수 실행을 시작할 때의 시점을 나타내는 DOMHighResTimeStamp() 단일 인자를 전달한다.

반환 값

requestAnimationFrame()을 취소할 수 있는 고유 id인 long 정수값이 반환된다.
window.cancelAnimationFrame()함수로 전달하여 취소할 수 있다.

차이점

setInterval과는 다음과 같은 차이점이 있다.

주사율만큼의 interval

setInterval을 사용하여 구현할 경우 interval을 손수 설정해주어야 한다.
requestAnimationFrame은 주사율만큼의 Interval을 가지게 된다.(설정해줄 필요가 없다.)

동시 실행

여러개의 setInterval을 사용할 경우 콜백이 겹쳐져서 버벅임이 발생할 수 있다. requestAnimationFrame을 사용할 경우 계속 실행하기 위해선 내부 callback에서 반드시 재호출해야 하므로 여러개의 애니메이션을 써도 버벅이지 않는다.

비동기

js-que

위의 동시실행과 같은 이야기지만 setTimeout, setInterval은 마이크로 태스크큐에서 작동한다. 하지만 requestAnimationFrame은 Animation Frame에서 동작한다. 때문에 setInterval과 달리 callback이 유실될 가능성이 없다.

예시

1
2
3
4
5
6
7
8
9
10
11
12
let start = null;
let hi = 0;
function callback(timestamp) {
if (!start) start = timestamp;
console.log("cnt", hi++);

if (timestamp - start <= 1000) {
window.requestAnimationFrame(callback);
}
}
window.requestAnimationFrame(callback);
// 61번 출력되었다.

Ref

window.requestAnimationFrame()

will-change속성으로 하드웨어 가속 적용하기

will-change 속성으로 하드웨어 가속을 적용해보자

하드웨어 가속

하드웨어가속은 gpu를 이용하여 cpu의 작업량을 줄이고 렌더링을 최적화하는 것을 말한다.

하드웨어 가속을 지시하게 되면 대상이 되는 요소만 존재하는 별도의 레이어를 만들어 작업하고 이 레이어를 페이지에 합친다.

즉 하드웨어 가속을 이용하면 보다 빠르게 렌더링 할 수 있다.

하지만 이 방법은 3D transforms에만 적용되고 2D transforms에는 적용되지 않는다.

2d 애니메이션의 경우 자동으로 하드웨어 가속이 활성화되지 않는다.

때문에 아래와 같이 강제로 3d로 변형시켜서 하드웨어 가속을 지시하곤 했다.

1
2
3
.accelerate {
-webkit-transform: translate3d(0, 0, 0);
}

현재는 2d 애니메이션의 will-change 속성을 이용하여 3d로 변형하지 않고도 브라우저에게 하드웨어 가속을 지시할 수 있다.

will-change

will-change 속성을 사용하여 브라우저에게 미리 변경사항을 알릴 수 있다.

이를 통해 애니메이션이 실제로 시작되기 전에 애니메이션을 준비하는데에 비용이 많이 드는 작업을 수행할 수 있다.

즉 3d로 변환하지 않고 브라우저에 변경사항을 전달하여 메모리를 최적화하고 할당하도록 할 수 있다.

사용방법

will-change속성은 이미 애니메이션이 적용되는 요소에 적용하는 것은 의미가 없다.

즉 브라우저가 사전에 최적화를 수행할 수 있도록 빠른 미래에 예상되는 변경 사항을 브라우저에 미리 알려야 한다.

최적화가 되지 않는 예시는 다음과 같다.

1
2
3
4
5
6
.will-change:active {
will-change: transform;
transition: transform 0.3s;
transform: scale(1.5);
}
/* 변경이 발생되는 시점에 속성을 지정한 경우 동작하지 않는다. */

요소가 active상태가 되기 위해선 먼저 요소를 hover 해야한다.
이를 이용하여 will-change를 적용한 예시는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
.will-change {
transition: transform 0.3s;
}

.will-change:hover {
will-change: transform;
}

.will-change:active {
transform: scale(1.5);
}

will-change 속성을 사용하게 되면 해당 변경사항이 항상 곧 일어난다고 브라우저에게 전달하게 된다.
이는 브라우저의 메모리 소비를 증가하게 만든다.
때문에 will-change는 적절한 시기에 추가하고 제거되어야 한다.

아래의 코드는 hover시의 애니메이션에 will-change를 적용한 코드이다.
다음과 같이 작성할 경우 브라우저의 메모리 소비는 증가하게 된다.
항상 will-change 속성이 적용되기 때문에 메모리 소비가 증가한다.

1
2
3
4
5
6
7
8
.will-change {
will-change: transform;
transition: transform 0.3s;
}

.will-change:hover {
transform: scale(1.5);
}

이는 다음과 같이 수정할 수 있다.
대상의 부모 요소가 hover되었을 때 will-change를 추가하여 브라우저에게 미리 최적화를 지시할 수 있다.

1
2
3
4
5
6
7
8
9
10
.will-change {
transition: opacity 0.3s linear;
}
.will-change-parent:hover .will-change {
will-change: opacity;
}

.will-change:hover {
opacity: 0.5;
}

허나 슬라이드와 같이 항상 변경되는 컴포넌트의 경우 다음과 같이 항상 변경에 대비하도록 작성하는게 더 낫다.

즉 사용자가 동일한 액션을 반복해서 수행할 것으로 예상되거나 계속 사용되는 인터액션의 경우 계속 유지하는 것이 더 낫다.

1
2
3
.slide {
will-change: transform;
}

주의사항

위에서 언급한 것 처럼 will-change속성은 브라우저가 최적화를 위한 작업을 유지하도록 하기 때문에
오래 유지되지 않도록 작업이 끝난 이후에 속성을 제거해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
var el = document.getElementById("element");
el.addEventListener("mouseenter", hintBrowser);

el.addEventListener("animationEnd", removeHint);

function hintBrowser(event) {
event.target.style.willChange = "transform, opacity";
}

function removeHint(event) {
event.target.style.willChange = "";
}

ref

하드웨어 가속에 대한 이해와 적용
An Introduction to the CSS will-change Property
CSS will-change 프로퍼티에 관해 알아둬야 할 것

css를 이용하여 text에 gradient 적용하기

text에 gradient을 적용하는 방법에 대해 알아보았다.

text에 gradient를 적용하려면 다음과 같은 속성을 적용시켜야 한다.

1
2
3
4
-webkit-background-clip: text; /* 배경이 text에만 적용됨 */
-webkit-text-fill-color: transparent; /* text색을 투명하게 만듬*/
width: fit-content; /* 옵션) content만큼의 width를 가지도록 함*/
background: linear-gradient(...); /* 선형 그라데이션을 배경으로 하게 함 */

이를 사용하여 간단한 예시를 작성해보았다.

1
2
3
4
5
<h1 class="gradient1">HELLO WORLD!</h1>
<div class="gradient1"></div>

<h1 class="gradient2">HELLO WORLD!</h1>
<div class="gradient2"></div>

다음과 같은 css를 추가하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
h1[class] {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
width: fit-content;
}

div[class] {
width: 100px;
height: 100px;
border-radius: 5rem;
}

.gradient1 {
background: linear-gradient(45deg, red, blue);
}

.gradient2 {
background: linear-gradient(45deg, red 50%, blue 50%);
}

결과물은 다음과 같다.

example

ref

CSS3 폰트 글자에 그라데이션 그라디언트 넣기 (Font Gradient)
linear-gradient mdn

iframe의 문제점과 HTML에서의 대안

HTML iframe 태그엔 여러 문제점이 있다.

iframe

iframe 요소는 현재 문서 안에 다른 HTML 페이지를 삽입합니다.

문제점

  1. 유지보수를 힘들게 한다.
  2. 브라우저 메모리와 컴퓨터 자원 사용량이 늘어난다.
  3. 검색엔진에 따라 iframe으로 삽입된 컨텐츠를 크롤링하지
  4. XSS공격에 취약해진다.
  5. 브라우저의 뒤로가기 버튼이 간헐적으로 작동되지 않게 하고 해상도에 따라 이상하게 보인다.
  6. 화면의 크기가 작을경우 프레임을 사용하지 못할 수 있습니다.

대안

대안으로 사용할 수 있는 태그들은 있지만 HTML을 삽입해야하는 경우엔 iframe이 제일 좋다고 한다.

embed
object

ref

iframe mdn
[Javascript] iframe 개념/장점/단점
HTML에서 Iframe의 대안

where 선택자 vs is 선택자

다중 요소를 선택할 수 있는 where 선택자와 is 선택자에 대해 알아보자

where와 is 없이 다중 요소에 css적용하는 방법

1
2
3
4
<header><div>헤더</div></header>
<main><div>메인</div></main>
<aside><div>어사이드</div></aside>
<footer><div>푸터</div></footer>

위 4개의 div의 글자색을 blue로 하는 css 코드는 다음과 같다.

1
2
3
4
5
6
header div,
main div,
aside div,
footer div {
color: blue;
}

where 와 is 사용하여 다중 요소에 css적용하는 방법

where나 is를 사용하면 더 짧게 작성할 수 있다.

1
2
3
4
5
6
7
:where(header, main, aside, footer) div {
color: blue;
}

:is(header, main, aside, footer) div {
color: blue;
}

where와 is

where는 명시도가 0이고, is는 명시도가 where보다 크다.(요소 선택자와 같다.)

두 개의 선택자 모두 낮은 명시도를 가지고 있다.

명시도 예시

몇 가지 명시도 예시를 살펴보자

where vs 요소 선택자 vs is

where 선택자의 명시도는 0이기 때문에 충돌시 요소 선택자가 적용된다.

is 선택자는 요소 선택자와 명시도가 같기 때문에 충돌시 늦게 작성된 선택자가 적용된다.

1
2
3
<div>
<p>hi</p>
</div>

따라서 다음과 같은 css를 적용시 color grey가 먼저 적용된다.

1
2
3
4
5
6
7
8
9
10
11
:where(div) p {
color: blue;
}

div p {
color: red;
}

:is(div) p {
color: grey;
}

하지만 is와 요소 선택자의 순서를 바꿀경우 color red가 먼저 적용된다.

1
2
3
4
5
6
7
8
9
10
11
:where(div) p {
color: blue;
}

:is(div) p {
color: grey;
}

div p {
color: red;
}

HTML 드래그 앤 드롭 API + React

HTML 드래그 앤 드롭 API를 이용하여 사용자는 draggable요소를 마우스로 선택해 droppable 요소로 드래그 하고 마우스 버튼에서 손을 뗌으로써 요소를 드롭할 수 있다. 드래그 하는 동안 draggable 요소는 반투명한 채로 마우스 포인터를 따라다닌다.

드래그 이벤트

드래그 앤 드롭은 DOM event model과 drag events를 mouse events로부터 상속받는다.

draggable 요소를 마우스로 선택하고, 마우스 포인터를 droppable요소로 가져가 마우스 버튼을 떼는 것으로 이루어진다.

다음과 같은 드래그와 관련된 속성이 있다.

이벤트이벤트 핸들러설명
dragondrag요소나 텍스트 블록을 드래그 할 때 발생한다.
dragstartondragstart사용자가 요소나 텍스트 블록을 드래그하기 시작했을 때 발생한다.
dragendondragend드래그를 끝냈을 때 발생한다. (마우스 버튼을 떼거나 ESC 키를 누를 때
dragenterondragenter드래그한 요소나 텍스트 블록을 적합한 드롭 대상위에 올라갔을 때 발생한다.
dragexitondragexit요소가 더 이상 드래그의 직접적인 대상이 아닐 때 발생한다.
dragleaveondragleave드래그하는 요소나 텍스트 블록이 적합한 드롭 대상에서 벗어났을 때 발생한다.
dragoverondragover요소나 텍스트 블록을 적합한 드롭 대상 위로 지나갈 때 발생한다. (짧은 간격의 ms마다 한 번씩 발생한다)
dropondrop요소나 텍스트 블록을 적합한 드롭 대상에 드롭했을 때 발생한다.

드래그 앤 드롭 적용하기

드래그 앤 드롭을 리액트에 적용해보았다.

1 요소를 draggable로 설정하기

요소를 만들고 요소를 draggable로 설정해주어야 한다.

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
import React, { useState, useRef } from "react";
import "./App.css";

const App = () => {
const [list, setList] = useState([
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
]);

return (
<>
{list &&
list.map((item, index) => (
<div
style={{
backgroundColor: "lightblue",
margin: "20px 25%",
textAlign: "center",
fontSize: "40px",
}}
key={index}
draggable
>
{item}
</div>
))}
</>
);
};
export default App;

2 드래그 시작 이벤트 추가하기

드래그가 시작될 때 어떤 요소로부터 시작되었는지 추적해야한다.

즉 dragStart이벤트의 target을 추적해야한다.

useRef를 사용하여 드래그가 시작된 아이템을 저장하였다.

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
import React, { useState, useRef } from "react";
import "./App.css";

const App = () => {
const dragItem = useRef();
const [list, setList] = useState([
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
]);

const dragStart = (e, position) => {
dragItem.current = position;
};

return (
<>
{list &&
list.map((item, index) => (
<div
style={{
backgroundColor: "lightblue",
margin: "20px 25%",
textAlign: "center",
fontSize: "40px",
}}
onDragStart={(e) => dragStart(e, index)}
key={index}
draggable
>
{item}
</div>
))}
</>
);
};
export default App;

3 드래그 하는 동안 건너뛰는 다른 요소 추적하기

드래그 하는 동안 다른 드래그 가능한 요소를 지날 경우 이 요소를 저장해야 한다.

즉 dragenter가 제일 최근에 일어난 요소를 저장해야한다.

useRef를 사용하여 저장하였다.

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 React, { useState, useRef } from "react";
import "./App.css";

const App = () => {
const dragItem = useRef();
const dragOverItem = useRef();
const [list, setList] = useState([
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
]);

const dragStart = (e, position) => {
dragItem.current = position;
};

const dragEnter = (e, position) => {
dragOverItem.current = position;
};

return (
<>
{list &&
list.map((item, index) => (
<div
style={{
backgroundColor: "lightblue",
margin: "20px 25%",
textAlign: "center",
fontSize: "40px",
}}
onDragStart={(e) => dragStart(e, index)}
onDragEnter={(e) => dragEnter(e, index)}
key={index}
draggable
>
{item}
</div>
))}
</>
);
};
export default App;

4 드래그가 끝났을 경우의 이벤트 추가하기

앞서 useRef에 저장해둔 두 요소를 바꾸는 이벤트를 추가한다.

즉 dragEnd가 되었을 때의 이벤트를 추가한다.

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
49
50
51
52
53
54
55
56
57
import React, { useState, useRef } from "react";
import "./App.css";

const App = () => {
const dragItem = useRef();
const dragOverItem = useRef();
const [list, setList] = useState([
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
]);

const dragStart = (e, position) => {
dragItem.current = position;
};

const dragEnter = (e, position) => {
dragOverItem.current = position;
};

const drop = (e) => {
const copyListItems = [...list];
const dragItemContent = copyListItems[dragItem.current];
copyListItems.splice(dragItem.current, 1);
copyListItems.splice(dragOverItem.current, 0, dragItemContent);
dragItem.current = null;
dragOverItem.current = null;
setList(copyListItems);
};

return (
<>
{list &&
list.map((item, index) => (
<div
style={{
backgroundColor: "lightblue",
margin: "20px 25%",
textAlign: "center",
fontSize: "40px",
}}
onDragStart={(e) => dragStart(e, index)}
onDragEnter={(e) => dragEnter(e, index)}
onDragEnd={drop}
key={index}
draggable
>
{item}
</div>
))}
</>
);
};
export default App;

5 dragover시 기본 애니메이션 효과를 제거한다.

dragover에 preventDefault를 호출해야 드롭을 허용할 수 있다.

정확히는 드롭이 허용된 애니메이션을 줄 수 있다.

1
onDragOver={(e) => e.preventDefault()}

++ dragEnter 될 떄마다 바뀌도록 하기

dragEnter 핸들러와 dragEnd 핸들러를 합치고 요소가 바뀔 때마다 dragItem이 dragOverItem을 참조하도록 하여 dragEnter 이벤트에 배열이 바뀌도록 설정할 수도 있다.

요소가 다른 요소를 통과할 때마다 배열이 바뀐다.

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
49
50
51
52
53
54
55
56
function App() {
const dragItem = useRef(null);
const dragOverItem = useRef(null);
const [list, setList] = useState([
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
]);

const dragStart = (e, position) => {
dragItem.current = position;
};

const dragEnter = (e, position) => {
dragOverItem.current = position;
const copyListItems = [...list];
const dragItemContent = copyListItems[dragItem.current];
copyListItems.splice(dragItem.current, 1);
copyListItems.splice(dragOverItem.current, 0, dragItemContent);
dragItem.current = dragOverItem.current;
dragOverItem.current = null;
setList(copyListItems);
};

// const drop = (e: React.DragEvent<HTMLDivElement>, position: number) => {};

return (
<>
{list &&
list.map((item, index) => (
<div
style={{
backgroundColor: "lightblue",
margin: "20px 25%",
textAlign: "center",
fontSize: "40px",
}}
onDragStart={(e) => dragStart(e, index)}
onDragEnter={(e) => dragEnter(e, index)}
// onDragEnd={drop}
onDragOver={(e) => e.preventDefault()}
key={index}
draggable
>
{item}
</div>
))}
</>
);
}

export default App;

여러개의 Promise를 다루는 다양한 방법 비교

js에서 비동기 호출을 다루는데엔 여러 방법이 있다.

인간 js엔진되기 2편에서도 다루었지만 더 명확하게 비교하여 정리해보려고 한다.

1. for each

여러개의 Promise가 들어있는 배열을 for each로 순회하며 호출할 수도 있다.

1
2
3
4
5
6
const urls = ["1", "2", "3", "4"];
urls.forEach(async (url) => {
const result = await fetch(url);
console.log(result.json());
});
console.log("complete");

실행결과는 다음과 같다.

1
2
3
4
5
complete
1
2
3
4

complete가 먼저 호출된 후 호출스택이 비었을 떄 1 2 3 4 순서대로 출력된다.
즉 배열 내 모든 Promise가 resolve 혹은 reject될 때까지 기다리지 않는다.

2. for await of

위와 같은 코드를 for await of 를 이용하여 작성해보자

1
2
3
4
5
6
const urls = ["1", "2", "3", "4"];
for await(let url of urls){
const result = await fetch(url);
console.log(result.json());
});
console.log("complete");

실행결과는 다음과 같다.

1
2
3
4
5
1
2
3
4
complete

1 2 3 4가 호출된 후 complete가 호출된다.
즉 배열 내 모든 Promise가 resolve혹은 reject될 때까지 기다린다.

3. Promise.all

위와 같은 코드를 Promise.all을 이용하여 작성해보자

1
2
3
4
5
6
7
8
const urls = ["1", "2", "3", "4"];
await Promise.all(
urls.map(async (ele) => {
result = await fetch(url);
console.log(result.json());
})
);
console.log("complete");

실행결과는 다음과 같다.

1
2
3
4
5
1
2
3
4
complete

즉 Promise.all도 배열 내 모든 Promise가 resolve혹은 reject될 때까지 기다린다.

하지만 for await of와 다르게 Promise의 순서를 보장하지 않는다.

아래와 같은 Timer 함수를 작성한다고 해보자

1
2
3
4
5
6
function timer(delay) {
console.log(`${delay} 타이머 시작`);
setTimeout(() => {
console.log(`${delay} 타이머 종료`);
}, delay);
}

for await of와 Promise.all에서 호출해보았다.

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
let times = [1000, 2000, 3000, 4000];
Promise.all(times.map((time) => timer(time)));
console.log("complete");

// 1000 타이머 시작
// 2000 타이머 시작
// 3000 타이머 시작
// 4000 타이머 시작
// 1000 타이머 종료
// 2000 타이머 종료
// 3000 타이머 종료
// 4000 타이머 종료
// complete

for await (let time of times) {
timer(time);
}

// 1000 타이머 시작
// 1000 타이머 종료
// 2000 타이머 시작
// 2000 타이머 종료
// 3000 타이머 시작
// 3000 타이머 종료
// 4000 타이머 시작
// 4000 타이머 종료
// complete

즉 for await of는 Promise간의 실행순서를 보장하지만 Promise.all은 그러지 못한다.

4. Promise.allSettled

Promise.allSettled는 Promise.all과 같은 기능을 하지만 Promise.all은 부분적으로 실패할 경우에 배열 전체의 실행이 중지되고 catch문으로 이동한다.
하지만 allSettled는 배열의 모든 Promise를 실행하고 상태와 결과값을 하나의 객체로 resolve문에 넘긴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) =>
setTimeout(reject, 100, "foo")
);
const promises = [promise1, promise2];

Promise.allSettled(promises).then((results) =>
results.forEach((result) => console.log(result))
);

// {status:'fulfilled', value:3}
// {status:'rejected',reason:'foo'}

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) =>
setTimeout(reject, 100, "foo")
);
const promises = [promise1, promise2];

Promise.all(promises)
.then((results) => results.forEach((result) => console.log(result.status)))
.catch((error) => console.log(error));

// foo

정리하자면 Promise.all은 일부 Promise에서 에러가 발생할 경우 모든 Promise의 실행을 중지하고 전부 에러를 발생시킨다.

하지만 Promise.allSettled는 일부 Promise에서 에러가 발생하더라도 모든 Promise가 실행될 때까지 기다린 후 Promise배열의 상태와 결과를 리턴한다.

5. Promise.race

Promise.race는 가장 먼저 완료된 Promise의 값으로 Promise를 reject하거나 resolve한다.

1
2
3
4
5
6
7
8
9
10
11
12
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "one");
});

const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "two");
});

Promise.race([promise1, promise2]).then((value) => {
console.log(value);
// two
});

정리

순서가 보장되어야 하는가? => for await of
일부 Promise에서 에러가 발생할 경우 전체의 실행을 멈추고 싶은가? => Promise.all
일부 Promise에서 에러가 발생하더라도 에러가 발생한 일부 Promise만 따로 예외처리 해주고 싶은가? => Promise.allSettled
가장 먼저 실행이 된 Promise의 결과만 다루고 싶은가? => Promise.race

css로 글자의 라인수를 제한하는 방법

css로 글자의 라인수를 제한하는 방법에는 여러가지가 있다.

한 줄로 제한하기(white-space)

css로 최대 라인 수를 한 줄로 제한하고 마지막에 …을 넣어줄 수 있다.

1
2
3
4
5
6
white-space: nowrap;
/* 텍스트가 띄어쓰기 되지 않게 한다 */
overflow: hidden;
/* 띄어쓰기 되지 않게 하고 넘친 부분은 숨긴다.*/
text-overflow: ellipsis;
/* 요소에 넘치는 부분을 ...로 대체하게 된다.*/

두 줄로 제한하기(webkit-line-clamp)

한 줄이 아닌 여러줄로도 제한할 수 있다.

-webkit-line-clamp 속성을 통해 최대 줄 수를 제한할 수 있다.

이를 사용하기 위해선 아래와 같은 css 속성 또한 적용해야 한다.

1
2
3
4
display: -webkit-box (또는 -webkit-inline-box)
-webkit-box-orient: vertical
-webkit-line-clamp: 3 (integer)
overflow: hidden;

최종 결과는 다음과 같다.

1
2
3
4
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;

ref

CSS로 텍스트 라인수를 제한하는 방법

-webkit-line-clamp mdn