전세계에는 영어처럼 왼쪽에서 오른쪽으로 쓰는 언어(LTR)뿐만 아니라 아랍어, 히브리어처럼 오른쪽에서 왼쪽으로 쓰는 언어(RTL)도 많이 있습니다
웹 페이지를 다국어로 지원할 때 단순히 left, right 같은 물리적 속성만 사용하면, RTL 언어에서는 레이아웃이 의도와 다르게 표시되는 문제가 발생합니다. 왜냐하면 예를 들어 left는 문서의 방향과 무관하게 항상 왼쪽을 의미하기 때문에, RTL 문서에서도 그대로 왼쪽에 적용되어 “시작 부분”에 적용되지 않기 때문입니다
이러한 문제 때문에 RTL 지원을 위해 별도의 CSS를 작성하거나, dir="rtl"일 때 클래스를 다르게 적용하는 등의 추가 작업이 필요해지곤 합니다.
3. Tailwind에서 기존 클래스를 덮어쓰는 방법
Tailwind CSS는 유틸리티 퍼스트(utility-first) 프레임워크로, 기본적으로 물리적 속성 기반의 클래스(.ml-4는 margin-left 등)를 제공합니다. Tailwind 자체적으로는 (과거 버전 기준) 논리적 속성 전용 유틸리티를 제공하지 않았기 때문에, RTL 지원을 위해서는 플러그인이나 커스터마이징이 필요했습니다. 다행히 Tailwind v3부터는 일부 논리적 속성을 다루는 유틸리티가 도입되었는데요. 예를 들어 ms-4는 margin-inline-start: 1rem, me-4는 margin-inline-end: 1rem에 해당하여, 콘텐츠 방향에 따라 좌우 마진을 알아서 적용해줍니다
이미 tailwind를 사용하면서 기본 값들 (w, ml-1 등등..)에 익숙해졌는데 새로운 값을 배우려니 귀찮기도 하고,
혹시 크로스 브라우징 이슈가 발생한다면.. (caniuse에 따르면 ie가 아니면 거의 문제없긴 합니다) 롤백이 쉽도록
직접 커스텀 플러그인으로 기존 위치 정보(width, left 등)을 logical properties로 overwrite하는 플러그인을 구현해서 사용해봤습니다. .ml-<값> 클래스가 margin-inline-start를, .mr-<값> 클래스가 margin-inline-end를 지정하도록 유틸리티를 추가하면 됩니다.
// tailwind.config.js의 plugins 배열 안
plugins: [
function ({ addUtilities, theme, variants }) {
const spacing = theme("spacing");
const newUtilities = {};
for (const [key, value] of Object.entries(spacing)) {
newUtilities[`.ml-${key}`] = { "margin-inline-start": value };
newUtilities[`.mr-${key}`] = { "margin-inline-end": value };
}
addUtilities(newUtilities, variants("margin"));
}
]
이렇게 하면 Tailwind의 기존 .ml-1, .mr-1, .ml-2, .mr-2, ... 클래스들이 모두 물리적 margin-left/right 대신 논리적 margin-inline-start/end로 동작하도록 덮어쓰게 됩니다.
- It ensures that pages from different websites are always put into different processes.
- It also blocks the process from receiving certain types of sensitive data from other site
즉, Chrome's Site Isolation effectively makes it harder for untrusted websites to access or steal information from your accounts on other websites.
Cross-Origin Read Blocking(CORB)
Cross-Origin Read Blocking (CORB) is an algorithm that can identify and block dubious cross-origin resource loads in web browsers before they reach the web page.
For example, it will block a cross-origin text/html response requested from a <script> or <img> tag, replacing it with an empty response instead.
CORS doesn’t explicitly allow access to the resource
그리고 CORB는 위 조건을 만족한다면
renderer process가 a cross-origin data resource를 받아와 사용하는 것을 금지한다.
(a new security feature that prevents the contents ofbalance.json from ever entering the memory of the renderer process memory based on its MIME type.)
참고)
X-Content-Type-Options: nosniff는 Microsoft에서 제안하는 확장 헤더이며 웹서버가 보내는 MIME 형식 이외의 형식으로 해석을 확장하는 것을 제한하는 크로스사이트스크립트 방어법이다.
즉, nosniff 옵션이 켜있다면 브라우저는 이게 HTML, XML, JSON중 하나인것으로 Content-type 종류로 인하여 헷갈리지 않고 바로 확정 지을 수 있다.
(가끔 어떤 서버는 이미지에 대해 text/html로 잘못 분류하기도 한다. 이걸 방지 가능)
이점
CORB를 사용하면 다음과 같은 이점이 있다.
- Mark responses with the correct Content-Type header.
- out of sniffing by using the X-Content-Type-Options: nosniff header.
- 보안! (In browsers with Site Isolation, it can keep such data out of untrusted renderer processes entirely, helping even against side channel attacks like Spectre)
브라우저 관점에서 입력이란 모든 사용자의 제스처(이벤트)를 의미한다. 스크롤도 이벤트이고, 화면 터치, 마우스 포인터를 올리는거도 입력 이벤트이다.
화면 터치와 같은 사용자 제스처가 발생했을 때 가장 먼저 제스처를 수신하는 것은 브라우저 프로세스이다. 브라우저 프로세스는 제스처가 어디에서 발생했는지만 알고 있다. 탭 내부의 콘텐츠는 렌더러 프로세스가 처리해야 한다.
그래서 브라우저 프로세스는 이벤트 종류와 좌표값(x,y 등)만 렌더러 프로세스에 넘기게 된다.
렌더러 프로세스는 이벤트 대상과 이벤트 리스너를 실행해 이벤트를 적절히 처리한다.
스크롤의 예시로 렌더러 프로세스 내부동작을 알아보자.
그림 출처 : https://developers.google.com/web/updates/2018/09/inside-browser-part4#compositor_receives_input_events
스크롤할때 레스터화된 레이어를 컴포지터가 합성해 스크롤을 부드럽게 처리한다.
이벤트리스너가 없다면 컴포지터 쓰레드는 메인 쓰레드와 상관없이 스스로 합성해서 프레임을 만들어낼 수 있다.
그런데 이벤트가 달려있다면 다르게 동작하게 된다.
합성하는 쓰레드는 컴포지트 쓰레드이고,
이벤트를 실제로 처리하는 쓰레드는 JS를 동작시키는 메인 쓰레드이다.
어떻게 둘이 상호작용을 하게 될까?
고속 스크롤 불가 영역(non-fast scrollable region)
JS가 실행되는 작업은 메인 스레드의 영역이므로 이벤트 핸들러가 연결된 영역을 고속 스크롤 불가 영역(non-fast scrollable region)이라고 표시한다. 이 영역에서 이벤트가 발생하면 렌더러 프로세스는 우선 컴포지터 스레드에게 작업을 보내고, 컴포지트 스레드는 입력 이벤트를 메인 스레드로 보내야하는지 정보를 확인 후 넘겨준다.
만약 고속 스크롤 불가 영역이 아니라면 컴포지터 스레드는 메인 스레드를 기다리지 않고 새 프레임을 합성한다.
(컴포지터 스레드는 복사된 자신만의 레이어 트리를 가지고 있는다. - pending tree -> active tree?)
passive : true 옵션을 주면 부드럽게 스크롤이 된다는거까지는 알았다. 그런데 스크롤을 JS의 event.preventDefault() 함수로 제한한다면 쓰레드가 다르므로 event.preventDefault() 가 실행되기 전 이미 수직 스크롤이 실행될 수도 있다. 그래서 event.cancelable()를 사용하면 스크롤 시작 여부를 확인 가능하다.
document.body.addEventListener('pointermove', event => {
if (event.cancelable) {
event.preventDefault(); // block the native scroll
/*
* do what you want the application to do here
*/
}
}, {passive: true});
======
역주 :모질라에 따르면 passive : true는 event.preventDefault()가 내부에서 실행이 안될꺼라는 약속이라는데
By marking a touch or wheel listener as passive,
the developer is promising the handler won't call preventDefault to disable scrolling.
This frees the browser up to respond to scrolling immediately without waiting for JavaScript,
thus ensuring a reliably smooth scrolling experience for the user.
라는데 원글의 예시 코드는 이해를 돕기 위한 '예시'인지 좀 혼란스럽긴 하다..
일단 스택오버플로우와 원글의 공통점을 추론해보자면
passive : true를 설정하면 메인 쓰레드(JS)가 작동하지 않고 스크롤 이벤트때 컴포지트 쓰레드가 바로 동작하는 개념은 맞는듯 싶다.
======
(아무튼 계속 시작)
또는 아에 touch-action에서 아예 수직 스크롤 이벤트를 지워버릴수도 있다.
#area {
touch-action: pan-x;
}
이벤트 대상 찾기
컴포지터 쓰레드가 메인 스레드로 보낼때 이벤트 대상을 찾는 hit test를 한다.
이벤트가 발생한 좌표에 무엇이 있는지 확인을 위해 paint recoards data를 이용한다.
(브라우저의 렌더링 단계에서 파싱->스타일->layout->paint->composite 에서 paint)
(아마 opcacity, transform등의 정보를 따로 담고 있는 property tree의 정보도 참고하기 위해서
paint tree를 참고하는거로 추론?)
메인 스레드 이벤트 전송 최소화
일반적으로 모니터는 1초당 60번 (60프레임) 화면을 갱신한다. 그리고 애니메이션도 화면 갱신 주기에 맞춰야
부드럽게 움직인다.
일반적으로 touchEvent는 초당 60~120회 전달하고, 마우스는 초당 100회정도 전달한다.
즉, 화면 갱신보다 이벤트 갱신 주기가 더 짧아서 이벤트가 뚝뚝 끊겨보이는 상황을 연출할 수도 있다.
너무 과도한 호출로 버벅거림을 유발하는 이벤트
그래서 메인 호출이 과다해지는것을 막기 위해서 Chrome은 자체적으로 연속적(coalesces continuous)인 이벤트(wheel,mousewheel,mousemove,pointermove,touchmove)는 다음번 requestAnimationFrame() 메서드 실행 직전까지 전송하지 않고 기다리게 된다.
keydown,keyup,mouseup,mousedown,touchstart,touchend와 같은 비연속적인(discrete) 이벤트는 즉시 전달된다.
한 프레임 안에서 합쳐진 이벤트 정보를 얻기 위해 getCoalescedEvent() 사용
드로잉 앱 같은 경우 프레임 사이 사이 경로가 누락되 선을 매끄럽게 그리지 못할 수도 있다. 이때 getCoalesecedEvents를 사용하면 합쳐진 이벤트의 정보를 얻을 수 있다.
그리고 미디어 쿼리를 이용해 자신이 사용하는 미디어 타입에 걸맞는 css만 받아오고, 다른 기기의 css는 받아오지 않는 식으로 (용량을 줄여) 최적화 할 수 있다.
2) 스타일
dom tree와 CSSOM tree가 생성되었다면 결합해서 Render Tree를 만든다.
렌더링 트리에는페이지를 렌더링하는 데 필요한 노드만 포함된다.
그리고 어떤 렌더 객체는 DOM 노드에 대응하지만 트리의 동일한 위치에 있지 않다. float 처리된 요소 또는 position 속성 값이 absolute로 처리된 요소는 흐름에서 벗어나 트리의 다른 곳에 배치된 상태로 형상이 그려진다. 대신 자리 표시자가 원래 있어야 할 곳에 배치된다.
3) 레이아웃(재시작시 Reflow)
레이아웃은 요소들의 기하학적인 구조를 찾는 과정이다.
현재 viewport 기준으로 정확히 DOM이 어디있는지 위치, 크기를 계산한다.
만약 CSS나 돔의 layout과 관련된 요소(width, font-size 등)가 변경된다면 reflow 과정이 생길수도 있다.
추가적으로
display: none을 사용하면 렌더링 트리에서 요소가 완전히 제거된다.
visibility : hidden을 사용하면 레이아웃에서 공간을차지하지만 요소가 보이지 않는다.
그리고 레이아웃 트리를 순회하면서 속성 트리(property tree)를 만드는 작업이 페인트와 레이아웃 사이에 끼여있다.
속성 트리는 clip, transform, opacity등의 속성 정보만 가진 트리이다.
최신 Chrome에서는 이런 속성만 별도로 관리하고 각 노드에서는 속성 트리의 노드를 참조하는 방식으로 변경되고 있다.
4) 페인트(재시작시 repaint)
레이아웃 트리를 따라가 페인트 기록을 생성한다.
배경 이미지, 텍스트 색상, 그림자등 실제 레이아웃의 수치를 변형시키지 않는 스타일의 변경이 일어나면
repaint만 발생한다.
일반적으로 reflow가 발생되면 repaint도 같이 발생한다.
(레이아웃 수치를 다시 계산해서 배치를 해야하기 때문에)
5. 합성(composite)
페이지를 그리는 방법
컴포지팅은 한 페이지의 부분들을 여러 레이어로 나누고 그 것들을 각각 레스터화하며 컴포지터 스레드에서 페이지를 합성하는 기술이다. 그리고 만약 스크롤이 발생하면, 레이어들이 이미 레스터되었기 때문에, 해야 할 것은 새로운 프레임을 합성하는 것이다.
그리고 OS는 프로세스에게 메모리 한 조각을 주고 모든 상태 정보를 고유 메모리 공간에 저장할 수 있게 한다. 어플리케이션이 끝나면 프로세스도 사라지고 OS가 메모리를 해제한다.
(개인적 생각으로 여기서 말하는 메모리는 RAM 메모리를 뜻하는것으로 추론된다.)
프로세스간 통신 - Inter Process Communication (IPC)
IPC
그리고 프로세스는 다른 프로세스에게 별도의 작업을 수행하도록 요청 혹은 통신이 가능하다. 많은 어플리케이션이 이 방식을 채택하고 있어 워커 프로세스가 무응답 상태에 빠지더라도 어플리케이션의 다른 부분을 수행하고 있는 프로세스들을 종료할 필요 없이 해당 프로세스만 재시작할 수 있다.