페이지 이동 후 쿠키가 사라지는 문제(Next.js, setHeader, writeHead)

설정한 쿠키가 페이지 이동 후 사라질 때 의심해 볼 상황

getServerSideProps에서 writeHead 또는 setHeader로 쿠키를 설정하고 redirect 시

등록된 쿠키가 사라지는 상황이 있다.

redirect된 페이지에서 쿠키가 사라진다면 헤더 설정의 문제를 확인해 봐야 한다.

합리적으로 의심해 볼 수 있는 문제는 페이지 이동 후 쿠키가 사라졌으므로 쿠키의 사용 범위에 대한 설정 문제이다.

Path=/ 설정은 쿠키가 모든 경로에서 유효하도록 설정하는 옵션이다. 이 옵션을 통해 모든 페이지에서 쿠키에 접근할 수 있도록 설정하므로 해당 옵션이 누락되면 페이지 이동 시 쿠키가 사라지는 문제가 발생한다.

setHeader와 writeHead를 사용해 path=/를 포함하는 쿠키 설정 예제를 확인해보자.
HttpOnly는 클라이언트의 자바스크립트에서 쿠키에 접근할 수 없도록 설정하는 옵션이다.

export async function getServerSideProps({ req, res }) {

  res.setHeader('Set-Cookie', 'myCookie=value; Path=/; HttpOnly; Max-Age=3600');

  return {
    redirect:{
      destination: '/new-page', // redirect할 페이지
      permanent: false
    }
  }
}

writeHead의 사용도 확인해보자.

export async function getServerSideProps({ req, res }) {
  const cookieVal = 'myCookie=value; Path=/; HttpOnly; Max-Age=3600';

  res.writeHead(302, {
    Location: 'new-page', // redirect할 페이지
    'Set-Cookie': cookieVal,
    'Custom-Header': 'Custom', // 커스텀 헤더
  });
  
  res.end();

  return {
    props: {}
  };
}

writeHead의 첫 번째 파라미터는 HTTP 응답 상태 코드, 두 번째는 헤더 객체를 설정한다.

헤더 객체 내부의 Location 필드로 redirect 페이지를 설정할 수도 있고, return 문에 redirect를 전달하면서 페이지를 설정할 수도 있다.

Next.js의 기본 권장 방식은 return 문에서 redirect를 전달하는 방법이다.

그렇다면 writeHead와 setHeader의 차이점은 무엇일까?

  • writeHead는 상태 코드를 설정(setHeader는 불가능)할 수 있는 등의 세세한 설정을 할 수 있다.
  • setHeader는 응답이 시작(res.end())되기 전 여러 번 호출이 가능하지만 writeHead는 한번만 호출한다.

따라서 상태 코드와 여러 헤더를 동시에 설정하려면 writeHead, 개별적으로 헤더를 추가하고 수정할 때는 setHeader를 사용하는 것이 좋다.

이와 같이 Path=/를 추가하면 이동하는 페이지에 상관없이 애플리케이션에서 쿠키를 확인할 수 있다.


추가 참고 : Next.js 공식 문서(Setting Headers)

이미지 파일 종류 알아보기(WEBP, PNG, SVG, JPG, TIFF….)

Webp, png, svg, gif, psd, eps, tiff

이미지 종류는 크게 웹용(WEBP, PNG, SVG, JPG, GIF)과 인쇄용(PSD, EPS, TIFF)으로 나눌 수 있다.

1. 웹용 이미지 데이터

– SVG
벡터(Vector) 그래픽을 기반으로 하는 이미지 데이터. 크기를 조절해도 품질이 저하되지 않으며 텍스트 기반이므로 코드로 이미지를 수정할 수 있다. 로고, 아이콘, 일러스트 등에 적합하며 PNG보다 더 작은 용량을 유지할 수 있다.

– PNG
무손실 압축 중 작은 파일 용량이 장점인 래스터(Raster) 형식의 이미지 데이터. 그림이나 로고 등 간단한 이미지에 적합하며 SVG에 비해 브라우저 호환성, 투명 배경, 필터 효과, 복잡한 색상과 세부 사항 표현 등에 더 유용하다.
무손실 압축이지만 해상도 제한으로 인해 크기에 따라 품질이 저하될 수 있다.

– WEBP
구글이 개발한 이미지 형식으로 손실과 무손실 압축을 모두 지원한다. 손실은 JPG, 무손실은 PNG와 유사한 방식으로 품질을 유지하고 파일의 크기를 줄인다. 알파 채널을 지원하여 투명 배경을 사용할 수 있고 여러 장의 사진을 사용해 애니메이션을 만들 수 있다. 동일 품질 대비 JPG, PNG보다 약 30% 가량 크기가 작으며, 웹 이미지 최적화 등에 사용된다.

– JPG
JPEG라고도 하며, 사진이나 그라데이션 등 여러 색상을 포함하는 이미지에 적절하다. 손실 압축을 사용하므로 용량이 작지만 같은 이미지를 계속 JPG로 덮어쓰게 되면 계속되는 손실 압축으로 인해 화질 저하가 계속 발생하게 된다.

– GIF
색의 수가 256색으로 파일 용량이 작으며, 여러 이미지의 배열을 통해 움직이는 영상을 만들 수 있다. 무손실 압축 방식을 사용하지만 색상 수의 제한으로 인해 일부 사항이 손실될 수 있다.

2. 인쇄용 데이터

– TIFF
이미지를 압축하지 않고 저장하는 형식의 데이터. 인쇄 및 그래픽 작업에서 인기가 많으며 저장을 반복해도 화질 저하가 발생하지 않는다. 무손실 방식이므로 파일 크기가 크고 압축, 최적화가 부족하여 웹용으로는 적절하지 않으며, 대부분의 브라우저에서 제대로 지원되지 않는다.

– PSD
포토샵 저장 형식으로 레이어 정보를 포함해 저장할 수 있다. 레이어 정보를 포함하면 용량이 커지므로 저장 시 레이어 병합을 통해 용량을 줄일 수 있다. 포토샵 기능을 활용해 복잡한 이미지를 만들 수 있으며, 고해상도 이미지를 저장할 수 있다. 브라우저가 지원하지 않으므로 JPG, PNG등으로 변환해야 한다.

– EPS
어도비 PortScript로 만든 형식의 데이터. 저해상도와 고해상도를 가지고 있어 저해상도로 작업하고 고해상도로 출력할 수 있다. 벡터 그래픽 장점을 최대한 활용할 수 있으며, 주로 인쇄에 적합하다.

spec.template: Invalid value 에러


~~~ is invalid. spec.template: Invalid value

yaml을 작성하고 kubectl apply -f sample.yaml과 같이 적용할 때 spec.template에러가 뜨는 상황이 있다.

이 에러가 뜬다면 해당 yaml로 파드를 처음 생성하는 것이 아니며 ,수정 사항이 조건에 위배되기 때문이다.

spec.Completions
spec.Selector
spec.Template
 

이 항목들은 수정이 불가하도록 지정되어 있다. 따라서 spec.template를 수정하는 apply하는 상황에서 발생한 에러이다.

수정이 불가하므로 신규 생성만 가능하다.

간단한 해결 방법으로 해당 yaml을 kubectl delete -f sample.yaml로 제거한 뒤 다시 apply를 적용하면 에러가 해결되는 것을 확인할 수 있다.


참고 : https://github.com/kubernetes/kubernetes/issues/89657

bash에서 vim 설치(feat.한글 설정)

bash: vim: command not found

경량화를 추구하는 컨테이너에는 기본 에디터가 제공되지 않으므로 필요 시 직접 설치해야 한다.

간단하게 bash에서 vim 에디터를 설치하는 방법과 한글 설정(UTF-8, CP949)까지 알아보자.

1. vim 에디터 설치

$ apt-get update

Hit:1 http://deb.debian.org/debian bookworm InRelease
Get:2 http://deb.debian.org/debian bookworm-updates InRelease [55.4 kB]
Hit:3 http://deb.debian.org/debian-security bookworm-security InRelease
$ apt install vim

Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  libsodium23 vim-common vim-runtime xxd
Suggested packages:
  ctags vim-doc vim-scripts
The following NEW packages will be installed:
  libsodium23 vim vim-common vim-runtime xxd
0 upgraded, 5 newly installed, 0 to remove and 40 not upgraded.
Need to get 8962 kB of archives.
After this operation, 41.8 MB of additional disk space will be used.
Do you want to continue? [Y/n] 

bash에서 두 커맨드를 실행하고 y를 눌러주면 끝이다(권한 문제가 발생하면 sudo를 앞에 붙인다).

그런데 한글이나 일본어 등을 사용하려면 인코딩이 필요하다.

UTF-8을 설정해보자.

vim 인코딩 설정(UTF-8)

설치한 vim을 사용해 /etc/vim/vimrc을 열고 하단에 다음 내용을 추가한다.

" Source a global configuration file if available
if filereaedable("/etc/vim/vimrc.local")
  source /etc/vim/vimrc.local
endif

//추가하는 부분
set encoding=utf-8
set fileencodings=utf-8,cp949

utf-8은 mac, cp949는 windows에서 사용하는 인코딩 방식이다.

저장하면 vim에서 한글 등이 사람 언어로 표시된다.

알기 쉬운 BEM 알아보기(CSS)

block__element_modifier모듈 기반의 방법론 (+mix)

공식 문서(https://en.bem.info)에서는 BEM을 이렇게 설명한다.

BEM(Block, Element, Modifier)는 컴포넌트 기반 접근 방식이다. 기본 아이디어는 사용자 인터페이스(UI)를 독립된 블록으로 나누는 것이다. 이를 통해 복잡한 UI도 쉽고 빠르게 개발할 수 있으며, 기존 코드를 재사용할 수 있는 장점이 있다.

BEM은 모듈을 Block, Element, Modifier 단위로 분해하며, 각 앞 글자를 따서 BEM으로 부른다. BEM은 러시아 Yandex사에서 개발한 방법론으로 실제로 이 방법에 기반한 CSS 설계 기법이 널리 사용되고 있지만 BEM은 CSS에만 국한되지는 않는다.

그렇다면 BEM의 공통 규칙과 모듈의 개별 규칙을 알아보자.

CSS style을 위한 BEM 공통 규칙

– ID 셀렉터와 태그 셀렉터를 사용하지 않는다.
– 네스팅된 셀렉터 사용을 최소화한다.
– CSS 클래스 네이밍 컨벤션을 사용하여 이름 충돌을 피하고 셀렉터의 목적을 명확하게 한다.
– Blocks, Elements, Modifiers를 구분하여 사용한다.
– Blocks를 재사용한다.

BEM의 네이밍 규칙

모두 소문자를 사용하며, 두 단어는 하이픈(-)으로 연결한다.

Blockblock, block-two
Elementblock 이름을 상속받아 언더스코어 두 개(__)로 연결block__element, block-two__element-two
ModifierBlock/Element 이름 상속받아 언더스코어 하나(_)로 연결. 키-값 쌍은 스네이크 케이스(_)를 사용.
block_modifier, block__element_modifier, element_key_value
Modifier
(MindBEMding)
Block/Element 이름 상속받아 하이픈 두개(–)로 연결. 키-값에서 키는 생략가능.block–modifier, element–value

모듈의 개별 규칙

논리적, 기능적으로 독립되어 재사용이 가능한 모듈로 정의한다. 클래스 이름은 ‘형태’가 아니라 ‘목적, 용도’를 나타내야 한다.

EX) head, logo, menu (O)
blue-button, small-title (X)

block은 다른 block 내부에서도 사용할 수 있으며, modifier를 통해 위치나 모양을 변경할 수 있다.

그림 – block의 중첩(출처: en.bem.info)

Element

block을 구성하며 block 내부에서만 사용하는 모듈로 정의한다. 예를 들어 menu__item은 menu라는 block을 구성하며 menu 밖에서는 사용할 수 없으므로 element가 된다.

그림 – block을 구성하는 element(출처:en.bem.info)


element 클래스 이름도 block과 동일하게 ‘형태’가 아니라 ‘용도’를 나타내야 한다.
element는 선택적 요소이므로 사용하지 않아도 상관없지만 element 이름을 중첩하여 사용하지 않는다.

EX) block__element__element, menu__button__icon (X)

Modifier

block/element의 모양이나 동작을 정의하는 모듈이다.
modifier는 선택적 요소이므로 사용하지 않아도 상관없지만 단독으로는 사용할 수 없고 block/element와 함께 사용해야 한다.
같은 block이라도 modifier에 따라 모양이 변할 수 있으며, modifier는 런타임이나 block에 따라 바뀔 수 있다.
modifier 클래스 이름은 형태(크기, 색, 상태, 동작)을 나타내야 한다. 형태로 나누면 boolean, key-value 두 가지가 있으며, 각각의 예는 다음과 같다.

– boolean : ex) active, disabled
– key-value : ex) color_red, line_doubled (스네이크 케이스로 표현)

modifier의 수는 제한이 없으므로 여러 개를 붙일 수 있지만 동일한 스타일은 중복하지 않는다.

MIX

mix는 하나의 요소에 역할이 다른 여러 클래스를 사용하는 기법이다.
장점은 코드를 복사하지 않고 기존의 스타일과 조합해서 새로운 모듈로 사용할 수 있다.
block+element, block+block, element+element 등의 mix를 사용할 수 있으며, 샘플 코드를 통해 확인할 수 있다.

<!-- HTML -->
<div class="header">
  <div class="logo header__logo"></div>
</div>

/* CSS */
.logo {
  width:100px;
  height:50px;
}
.header__logo {
  margin: 12px;
}

다른 요소와 관계된 레이아웃과 관련된 부분은 mix, block 내부의 문제는 modifier를 사용하는 것이 좋다.
mix는 독립성과 재사용성이 높은 상태를 유지하는 장점이 있다.


참고
https://en.bem.info
– 다양한 예제로 배우는 CSS 설계 실전 가이드


CSS 설계를 위한 기본 가이드

변경과 확장에 강하고 안정적인 CSS 구조를 위한 기법

다양한 예제로 배우는 CSS 설계 실전가이드에서 소개하는 설계의 핵심을 알아보자.

CSS의 여러 설계 기법들도 결국에는 다음 여덟 가지 리스트에 속한다고 하니 핵심을 관통하는 포인트라고 볼 수 있다.

1. 특성에 따른 CSS 분류
2. 느슨한 스타일의 결합
3. 적당한 영향 범위
4. 특정 컨텍스트에 대한 적절한 의존도
5. 적절한 명시도(specificity)
6. 영향 범위가 유추 가능한 클래스 네임
7. 형태, 기능, 역할의 유추가 가능한 클래스 네임
8. 확장 용이성

1. 특성에 따른 CSS 분류

첫 번째는 CSS 역할이나 특성에 따라 분류하는 것이다.

베이스 그룹 : 사이트에서 베이스가 되거나 공통으로 적용되는 사항 등
레이아웃 그룹 : 헤더(header), 푸터(footer), 콘텐츠(content) 영역 등
모듈 그룹 : 재사용되는 모듈 등

모듈 자체에는 레이아웃과 관련된 부분은 설정하지 않는 것이 좋다. 모듈은 자신의 레이아웃에는 관여하지 않고 해당 모듈 자체의 역할 및 자녀 요소의 스타일링에만 관심을 가져야 한다. 여기서 레이아웃과 관련된 부분이란 다음과 같다.

-position, z-index, top/right/bottom/left, float, width, margin

/* 베이스 */
body {
  font-family: 'Noto Sans';
}

/* 레이아웃 */
layout_content {
  width: 1000px;
  padding: 10px;
  margin: 0 auto;
}

/* 모듈 */
.bl_media {
  display: flex;
  justify-items: center;
  align-items: center;
}

.bl_media_title {
  margin: 5px;
  font-weight: bold;
}

2. 느슨한 스타일의 결합

정확히는 HTML과 스타일링의 느슨한 결합이다. 느슨한 결합이란 의존하지 않는 상태를 이야기하는데 가장 간단한 방법으로는 요소형 셀렉터를 피하고 클래스 셀렉터를 사용하는 것이다.

p{}, div{}, h1{}과 같이 요소의 스타일을 직접 정의하는 요소형 셀렉터는 요소의 변경이 발생하게 되면 스타일이 중복되거나 누락될 가능성이 크므로 클래스를 사용해서 정의하는 것이 좋다.

3. 적당한 영향 범위

영향 범위를 가능한 좁게 하거나 영향 범위가 넓은 CSS의 스타일을 최소화한다.

영향 범위를 고려하지 않은 채 스타일을 추가하다보면 의도하지 않은 부분에서 오류가 발생하므로 최대한 범위를 줄이기 위해 가장 가까운 부모 요소를 포함시키거나 손자와 자녀 셀렉터의 사용도 함께 검토하는 것이 좋다.

4. 특정 컨텍스트에 대한 적절한 의존도

컨텍스트란 위치 또는 상황을 의미하며, 이 컨텍스트가 변할 때 코드가 동작하지 않을 수 있는 상황을 주의해야 한다. 사용 목적이 명확하다면 문제없지만 그렇지 않은 경우에는 셀렉터를 불필요한 부분까지 구체화하게 되면 해당 문제가 발생하기 쉽다.

/* id인 main을 지정하게 되면 해당 모듈 내부에서만 사용 가능한 .title_top 클래스가 되어 의존도가 높아짐. -> 불필요한 #main 제거 */
#main .title_top{
  display:flex;
  align-items: center;
  color: green;
}

5. 적절한 명시도(specificy)

우선순위를 나타내는 명시도와 관련된 내용은 여기를 클릭하면 확인할 수 있다.

명시도가 높은 CSS의 단점은 다음과 같다.

– 셀렉터 예측이 어려움
– 다른 요소(부모 요소 등)에 대한 의존도 상승
– 덮어쓰기의 어려움
– 유지 보수의 어려움

당연히 !important는 사용하지 않는 것이 기본이며, 셀렉터를 사용할 때는 클래스 셀렉터를 사용한다. id 셀렉터는 우선순위가 매우 높으며 한 페이지 안에서 동일한 값은 한번 밖에 사용하지 못하는 제약도 있으므로 id를 사용하는 이점은 많지 않다.

따라서 기본적으로 클래스 셀렉터를 사용해서 의도하지 않은 우선 순위가 뒤섞이지 않도록 해야 한다.

6. 영향 범위가 유추 가능한 클래스 네임

규모가 커질수록 모듈이나 클래스도 늘어나므로 하나의 클래스 수정이 어떤 범위까지 영향을 미칠지를 이름에서 판단할 수 있도록 해야 한다.

HTML에서 정의된 요소의 클래스 네임만 보고도 CSS의 영향 범위를 유추할 수 있도록 하자.

클래스 네임으로 영향 범위를 쉽게 유추하기 위해서는 자녀 요소에 루트 요소의 클래스 네임을 포함하도록 하는 것이 좋다.

7. 형태, 기능, 역할의 유추가 가능한 클래스 네임

영향 범위와 마찬가지로 형태, 기능, 역할도 유추가 가능하도록 네임을 지정해야 한다.

– title1, title2, title3
– main-title, nav-title, sub-title

두 항목 중 형태나 기능, 역할의 유추가 쉬운 쪽이 어디인지는 분명하다. 각 모듈의 기능이나 역할에 맞춰 이름을 붙이는 것이 매우 중요하다. 특히 작은 규모의 코드라면 title1, title2와 같이 네임을 대충 지정하기 쉽다. 하지만 규모란 언제 어떤 방식으로 커질지 모르므로 항상 습관적으로 이름에서 형태, 기능, 역할을 유추할 수 있고 구체성과 범용성의 균형을 맞추도록 하는 노력이 필요하다.

8. 확장 용이성

확장성이란 결국 기능의 추가나 유지와 연결되는 부분으로, 항상 가능성을 열어두어야 한다.

확장이 쉽도록 클래스를 설계하거나 추가하는 클래스에는 기능, 역할에 따라 적절한 상세도와 영향 범위를 갖도록 한다.

확장이 쉬운 클래스란 멀티 클래스를 사용하는 것으로, 하나의 클래스에는 보편적인 속성, 추가 클래스에는 특정 속성을 추가하여 덮어쓰기를 통해 여러 클래스로 스타일을 지정하는 방식이다.

이를 통해 HTML은 복잡해지지만 CSS는 매우 간단해지는 트레이드오프가 발생하기도 한다. 하지만 멀티 클래스의 이점은 불규칙한 상황에서도 클래스 하나만으로 원하는 작업을 완료할 수 있는 간결함이다.

결국 모든 항목은 별개의 사항이 아니라 다 연결되어 있음을 알 수 있고, 작은 수정 하나가 큰 나비효과를 불러올 수도 있다는 점을 명심해야 한다.

매번 하나하나 체크해가면서 코드를 작성하기보다는 작성 방식이 고민되거나 자율성이 주어졌을 때, 해당 리스트를 떠올리면서 작성하다보면 생각보다도 더 견고한 구조를 만들 수 있을 것이다.


참고 : 다양한 예제로 배우는 CSS 설계 실전가이드(2021, 제이펍)

자바스크립트, class를 문법적 설탕(syntactic sugar)이라 부르는 이유

function으로 구현하는 class의 동작 방식

‘문법적 설탕’은 통용되는 단어이지만 어감이 이상한 건 어쩔 수 없다. 의역하여 ‘편리한 구문 용법’ 정도가 더 이해하기 쉬울 것 같다.

위키에서 syntactic sugar의 정의는 다음과 같다.

In computer science, syntactic sugar is syntax within a programming language that is designed to make things easier to read or to express.
컴퓨터 과학 분야에서 ‘편리한 구문 용법’이란 읽기 쉽고 표현하기 쉽도록 구성된 프로그래밍 언어 내의 구문이다.

즉, 달콤한 초콜릿처럼 손이 자주 가는 구문이라고 볼 수 있을 것 같다.

자바스크립트는 처음부터 모든 기능을 갖고 태어나지 않았으므로 ES 버전으로 대표되는 추가 기능은 기존의 문법으로 구현된 것이 많다.

class 또한 기존의 OOP에서 차용한 개념을 자바스크립트에서 구현한 것으로 구현 방식은 자바스크립트의 문법을 벗어나지 않는다.

따라서 사용자가 선언하고 사용하는 방식은 다르지만 다음에서 확인할 두 코드의 작동 방식은 동일하다.

function으로 선언된 부분이 기본 동작 방식이며 class가 선언되면 이 function과 완전히 동일한 방식으로 구현된다.

같은 기능을 구현할 때 function과 class 중 어떤 것에 더 선뜻 손이 가는지를 묻는다면 설탕의 의미를 생각해볼 수 있다.

function 사용

function createGame( user, point ) {
  this.user = user;
  this.point = point;
}

createGame.prototype.addPoint = function() { this.point++; };

const myGame = new createGame("Yoon", 0);

myGame.addPoint();

class 사용

class CreateGame {
  constructor (user, point) {
    this.user = user;
    this.point = point;
  }
  addPoint() { this.point++; }
}

const myGame = new CreateGame("Yoon", 0);
myGame.addPoint();

React에서 iframe 전체화면 전환하기(속성 체크 포함)

allowfullscreen 그리고 document.fullscreenEnabled

화면 내부에 삽입된 iframe에서 전체 화면 설정, 해제 기능을 구현하려고 한다.
구현은 생각보다 간단하다.

먼저 iframe 태그에 allowfullscreen 속성을 추가해야 한다.
allowfullscreen 속성을 갖는 iframe은 자바스크립트에서 제공하는 메서드를 통해 전체화면 설정, 해제가 가능하며 ESC도 사용할 수 있다.

<iframe src=”https://choonse.com” allowfullscreen />

전체 화면 설정 메서드 document.documentElement.requestFullscreen()
전체 화면 해제 메서드 document.exitFullscreen()

전체화면 설정/해제 버튼에 이벤트를 걸어서 사용할 수 있으며, 반환형은 Promise이다.

만약 allowfullscreen 속성이 없는 iframe에서 해당 메서드를 호출하면 어떻게 될까?disallowed by permissions policy 에러로 사람을 당황시킨다.

이 때는 allowfullscreen 속성의 존재 여부(true)를 체크해주면 되는데, 해당 속성은
document.fullscreenEnabled 속성을 사용해 true/false로 확인한다.

만약 iframe에 allowfullscreen 속성이 없을 때 전체 화면 버튼을 비활성화하고 싶을 때는 어떻게 할까?

document.fullscreenEnabled를 체크해서 활성/비활성을 결정하면 되는데 document is not defined와 같은 에러를 피하기 위해서는 useEffect 내부에서 해당 작업을 처리하여 렌더링이 완료된 시점에 document에 접근하도록 해야 한다.

성능에 중요한 repaint, reflow에 대해 알아보자

reflow와 repaint가 발생하는 요인 분석하기

먼저 페이지가 렌더링되는 과정에서 reflow와 repaint의 단계는 다음과 같다.

(DOM + CSSOM) render tree > layout(reflow) > paint(repaint) > composite

여기서 성능과 관련이 깊은 reflow, repaint 단계에 관해 알아보자.

reflow

reflow(또는 layout)는 render tree 생성 후 요소의 크기와 위치를 계산하는 단계이다. 초기 렌더링 시 최초로 발생하고 요소의 크기나 위치 변경, 요소의 추가나 삭제 등 레이아웃이 변경될 때마다 reflow가 다시 발생한다.

reflow는 자식과 부모 그리고 요소 자신에게도 영향을 미치므로 성능에 영향을 주며, 레이아웃을 계산하는 reflow 후 가시적인 요소를 생성하는 paint 단계가 발생한다.

reflow가 발생하는 구체적인 상황은 다음과 같다.
(https://www.geeksforgeeks.org/what-is-dom-reflow/).

Reflow 발생 원인

– DOM에 요소 삽입, 제거, 업데이트
– 페이지에서 수정 작업 발생(Ex. input 박스 텍스트 수정)
– DOM의 요소 이동
– DOM의 요소 애니메이션 효과
– offsetHeight 또는 getComputedStyle()를 사용한 요소 계산
– CSS 스타일 변경
– 요소의 클래스 이름 변경
– 스타일시트 추가 또는 제거
– 윈도우 리사이징
– 웹 페이지 스크롤
– 폰트 변경
– CSS 슈도 클래스(의사 클래스) 활성화(Ex. :hover)
– 요소에 style 어트리뷰트 설정

reflow 단계에서 많은 연산이 발생하고 repaint, composite 단계도 이어서 발생하므로 성능에 영향을 줄 수 밖에 없다. 따라서 페이지를 구성하는 방식, 성능과 애니메이션의 타협 등을 통해 reflow를 최소화하는 방법을 찾아야 한다.

기본적으로 reflow를 줄일 수 있는 방법은 다음과 같다.

Reflow 줄이는 방법

– 여러 개의 인라인 스타일을 지정하지 않고 개별 스타일 설정을 최소화하기
– 요소에 클래스명을 사용하고 DOM 트리에서 최대한 하부에서 사용하기
– 새로운 스타일을 추가하기 전에 DOM에서 요소를 제거한 뒤 다시 추가하기
– 스타일을 위한 잦은 연산 피하기
– 애니메이션은 fixed 또는 absolute로 지정하기
– 가능한 테이블 레이아웃보다 블록 레이아웃을 사용하기
– 고정된 크기로 테이블 생성하기
– 클래스를 통해 스타일 변경하기
– 테이블 레이아웃을 fixed로 지정하기

REPAINT

repaint(또는 redraw)는 레이아웃이 아닌, 요소의 가시적인 부분이 변경되면 발생하는 단계이다. 요소의 visibility, background, outline 등이 변경될 때마다 repaint가 발생하지만 해당 속성은 레이아웃에 영향을 주지 않으므로 reflow는 발생하지 않는다.

repaint는 render tree를 레이어별로 나누어 처리하므로, repaint 발생 시 해당 레이어만 수정할 수 있는 장점이 있다.

Reflow와 Repaint는 stacking context와도 관련이 있는데, stacking context는 간단하게 3차원인 z축을 의미하는 것이다.

여러 요소가 하나의 레이어로 묶이게 되면 차지하는 메모리를 줄일 수 있지만 묶인 요소 중 하나의 요소에서 변경이 생기면 해당 레이어에 있는 요소를 모두 다시 그려야 하는 상황이 발생한다.

따라서 변경이 생길 가능성이 있는 요소를 하나의 레이어로 만들어서 관리하면 해당 레이어만 다시 그리면 되기 때문에 효율적으로 리렌더링을 할 수 있다.

이렇게 해당 요소를 하나의 레이어로 분리하기 위한 조건은 다음 사이트에서 확인할 수 있다.
stacking context 자세히 알아보기(MDN)

조건 중 ‘opacity < 1’이 있는데, opacity 값이 0~0.99인 상황에서만 레이어가 별도로 생성된다는 의미이다.

opacity가 1일 때는 reflow가 발생할 수 있는데 이는 opacity:1인 경우 다른 요소와 하나의 레이어를 이루기 때문에 발생한다고 볼 수 있다.

Composite

상황에 따라 Reflow, Repaint가 발생하지 않는 속성으로 transform, opacity 등이 있는데 이 속성은 렌더링의 마지막 단계인 composite만 실행된다.

해당 속성은 CPU가 아닌 GPU에서 작업을 수행하는 graphic layer를 사용하기 때문인데 composite 단계에서 layer를 병합한다.

GPU는 CPU에 비해 연산이 빠르고 이미지와 애니메이션의 보정 기능도 가지고 있으므로 이미지나 애니메이션 처리에 좋은 대안이 될 수 있다.

GPU를 사용하는 조건은 다음과 같다.

GPU 사용 조건

– 3D transforms 사용(translate3d, translateZ 등)
– <video>, <canvas>, <iframe> 요소 사용
– transform, opacity를 사용하는 애니메이션
– position:fixed 요소
– will-change 사용
– filter 사용

GPU의 장점은 빠른 연산이지만 메모리를 많이 차지하는 단점도 있으므로 적정선에서 사용해야 쾌적한 환경을 만들 수 있다.

참고
Understanding Repaint and Reflow in Javascript
What is DOM reflow
GPU animation: Doing It Right
Stacking Context

Typescript 유틸리티 타입(Partial, Pick, Omit, Record, Readonly)

공통 타입 변환을 위한 유틸리티 타입

타입스크립트에서 제공하는 유틸리티 타입(Utility Types)은 type, interface를 입맛에 맞게 사용할 수 있도록 도와주는 유용한 기능이다.

유틸리티 타입의 종류는 다음 그림과 같이 매우 많다(타입스크립트 공식 페이지).

이 중 사용 빈도가 잦은 Partial, Pick, Omit, Record, Readonly에 대해 알아보자.

1. Partial

partial은 ‘부분적인’이라는 뜻으로, 정의된 타입의 속성을 모두 옵셔널로 사용하여 부분적으로 원하는 속성만 사용하도록 할 수 있다.

우선 Partial의 정의는 다음과 같다.

type Partial<T> = {
  [P in keyof T]?: T[P];
};

Partial<Type>의 방식을 통해 속성을 옵셔널하게 사용하며, 예시는 다음과 같다.

 
interface Hardware{
    cpu: string;
    gpu: string;
    memory: number;
    storage: number;
    accessary: string;
}

const selectOptions = (default: Hardware, selected: Partial<Hardware>) => {
    setOptions({...default, ...selected});
};

const defaultOption = {
    cpu: "M1";
    gpu: "10-core";
    memory: 16;
    storage: 512;
    accessary: "Touch ID";
}

const selectedOption = {
    cpu: "M2";
    memory: "32";
    storage: "1024";
}

이와 같은 방법으로 사용하지 않는 속성을 옵셔널로 변경하여 사용할 수 있다.

2. Pick

pick은 ‘뽑다’는 뜻으로, 정의된 타입에서 속성을 지정하여 사용할 수 있다.

Pick의 정의는 다음과 같다.

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

Pick<Type, Keys>의 방식을 사용해 사용할 속성을 문자열이나 유니온 타입으로 Keys에 지정하여 해당 속성만 사용한다.

interface Exercise{
    title: string;
    type: string;
    time: number;
    complete: boolean;
}

type BriefExercise = Pick<Exercise, "title" | "complete">;

const today = {
    title: "jogging";
    complete: true;
}

Partial은 모든 속성을 옵셔널로 사용하지만 Pick은 속성을 지정하여 필요한 타입만 정의하는 방식으로 사용한다.

3. Omit

omit은 ‘생략하다’라는 뜻으로, 정의된 타입에서 생략할 속성을 지정하여 사용할 수 있다.

Omit의 정의는 다음과 같다.

type Omit<T, K extends keyof any> = {
  [P in Exclude<keyof T, K>]: T[P]
}

Omit<Type, Keys>의 방식으로 사용하며, Pick과 반대로 필요하지 않은 타입을 지정하여 정의하는 방식으로 사용한다.

interface Exercise{
    title: string;
    type: string;
    time: number;
    complete: boolean;
}

type BriefExercise = Omit<Exercise, "type" | "time">;

const today = {
    title: "jogging";
    complete: true;
}

Pick과 Omit의 용도는 같지만 정의 방법은 상반된다.

정의된 타입에서 생략하고 싶은 속성이 많을 때는 Pick, 생략하고 싶은 속성이 적을 때는 Omit을 사용하면 효과적이다.

4. Record

record는 ‘기록하다’는 뜻으로, key-value 세트를 생성하여 사용할 수 있다.

Record의 정의는 다음과 같다.

type Record<K extends string, T> = {
  [P in K]: T;
};

Record<Keys, Type>의 방식으로 사용하며, 두 개의 타입에서 한 타입을 Keys, 한 타입을 Type으로 조합하여 사용할 수 있다.

type MacBookType = "air13" | "air15" | "pro13" | "pro14" | "pro16";

interface Performance{
    cpu: string;
    memory: number;
    storage: number;
};

const OnTheMarket: Record<MacBookType, Performance> = {
    air13: { cpu:"M1", memory:8, storage: 256 };
    air15: { cpu:"M2", memory:16, storage: 512 };
    pro13: { cpu:"M2", memory:16, storage: 1024 };
    pro14: { cpu:"M2 pro", memory:32, storage: 1024 };
    pro16: { cpu:"M2 ultra", memory:96, storage: 2048 };
}

위와 같은 방식으로 두 타입을 조합해서 사용할 수 있다.

5. Readonly

readonly는 ‘읽기 전용’이므로 타입을 읽기 전용으로 정의하여 사용할 수 있다.

Readonly의 정의는 다음과 같다.

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

타입을 사용해 readonly 속성을 부여하고 싶은 경우에 사용할 수 있다.

interface LimitSize{
    size: number;
}

const myData: Readonly<LimitSize> = {
    size: 32;
}

myData.size = 64; // error