반응형

 

갑자기 부캠 프로젝트에서 시간 문제로 미완성 했었던

drag and drop 크기 변경 기능을 한번 구현해보고 싶어서 잠깐 해보았다.

 

레이아웃 틀만 잡아주고 팀원에게 넘기고 다른곳으로 넘어간곳이라 새로운 기능 추가가 어떤 사이드 이펙트를 낳을지 몰라 무섭긴 하지만... 프로젝트도 종료되었고 drag and drop 연습겸 해보았다!

 

기본 원리

const PAGEVW = 100;
const OPENSIZE = 82;
const SIDEBARSIZE = 18;
const CLOSEDSIZE = 61;

export const sizestate = atom<number>({
  key: 'sizeState',
  default: OPENSIZE,
});

구현할때 왼쪽부터 차례대로 Channels, Workspace(chat), Thread(reply)라고 나눴었고.

 

전체 width를 100(%)이라고 했을때

댓글창이 안열려 있다면 workspace의 width가 82(%)이고 channel이 18(%),

열려있다면 workspace의 width가 61(%)이고 나머지가 reply(21%) 창과 channel이 18(%)이 되도록 구현해놓은 상태였다.

 

그리고 위에서 구한 값을 recoil에 넣어 전역으로 관리했고, styled-component에 props로 넣어 각 화면의 width를 비율대로 맞추게 했다.

 

// workspace 길이 
export const mainWorkspaceSizeState = selector<number>({
  key: 'mainWorkspaceSizeState',
  get: ({ get }) => {
    const { isOpened } = get(replyToggleState);
    const OPENEDSIZE = get(sizestate);

    if (isOpened) return CLOSEDSIZE;
    return OPENEDSIZE;
  },
});

//reply 길이
export const replyWorkspaceState = selector<number>({
  key: 'replyWorkspaceSizeState',
  get: ({ get }) => {
    const { isOpened } = get(replyToggleState);

    if (!isOpened) return 0;

    const size = PAGEVW - get(mainWorkspaceSizeState) - SIDEBARSIZE;
    return size;
  },
});

(위 코드는 workspace 의 width를 기준으로 reply 창이 열렸을때, 안열렸을때 recoil의 selector를 이용해 각각 길이를 계산해주는 코드이다.)

 

그렇다면 여기서 넣어줄 기능은 drag and drop으로 동적으로 width를 변경할 수 있는 코드만 넣으면 되는데...

 

다음 방식을 사용했다.

 

저 빨간 선(div)를 구현한 코드는 다음과 같다.

 

import React from 'react';
import { useSetRecoilState } from 'recoil';
import { widthSizeState } from '@state/workspace';

import Container from './styles';

const DraggableDiv = (): JSX.Element => {
  const setWidthSize = useSetRecoilState(widthSizeState);

  const dragEventHandler = (e: React.DragEvent) => {
    const { target } = e;
    const { parentNode } = target;

    const parentBox = parentNode.getBoundingClientRect();
    setWidthSize(Math.max(47, (e.clientX / parentBox.width) * 100 + 1));
  };

  return <Container draggable="true" onDragEnd={dragEventHandler} />;
};

export default DraggableDiv;

원리는 상당히 간단하다.

 

drag 하고 마우스를 놨을때 drag가 끝난 점의 좌표를 계산해 넣어준다.

클릭한 점의 X 좌표를 빨간선 div의 부모 width로 나누고 100을 곱하면 백분위가 되는데

왜냐하면 전체 박스 길이(width)에서 맨 왼쪽을 기준으로 오른쪽만큼 간게 e.clientX가 되므로

단순히 나눠주고 100을 곱하면 백분위가 된다.

 

참고) draggable 이 달려있는게 빨간선이다.

 

그렇게 구한 값을 recoil에 적용해주면 workspace의 width이고, selector를 통해 reply의 크기가 자동으로 반영되므로 

화면의 크기를 동적으로 바꿀 수 있게된다.

 

 

channel도 동일하게 drag and drop을 붙일 수 있는데 노가다의 문제이므로 

drag and drop을 써본거에 만족하고 끝내려 한다. 

 

(구현하려면 DraggableDiv에서 setRecoilValue만 바꿔주면 되는데 위에서 주입하거나 view와 logic을 분리해서 구현하면 될꺼같다?)

반응형

'Front-end > React' 카테고리의 다른 글

react 17(개인 공부용)  (1) 2022.06.09
React18(개인 공부 메모용)  (0) 2022.06.09
react TMI  (0) 2022.04.13
redux, redux-saga로 로딩, 에러 페이지 구현  (0) 2022.02.12
redux 기본 구조  (0) 2022.02.02
반응형

SPA(Single Page Application)이란? 

 

과거에는 웹 페이지를 클릭하면 하나의 문서를 전달하고, <a href="페이지 주소"> 같은 하이퍼 링크를 누르면

새로운 페이지를 서버에서 전송받아 보여주고는 했습니다.

즉, 새로운 페이지를 요청할때마다 정적 리소스가 다운로드 되고 (새로운) 전체 페이지가 전부 다 렌더링 됩니다.

 

SPA와 기존 방식(MPA)

SPA는 기존 방식과 달리 웹 페이지에 필요한 리소스(HTML,CSS,JS)를 한번에 다 다운로드 받은 뒤

새로운 페이지 요청이 있을 경우 페이지 갱신에 필요한 데이터만 서버에서 전달받아 페이지를 갱신하는 방식(AJAX)으로 구현이 가능합니다.

 

즉, 빈 도화지(HTML)에 그림(JS로 동적 렌더링)을 그리는 식으로 작동합니다.


JS로 간단하게 구현해봅시다.

 

폴더 구조

└─src
|   ├─pages
|   |   ├─ RouterButton.js
|   |   ├─ A.js
|   |   ├─ B.js
|   |   └─ C.js
|   ├─BaseComponent.js 
|   └─App.js
└─ index.html

page는 A,B,C 페이지가 있고

요청에 따라 index.html에 동적으로 A,B,C 페이지를 각각 그려줄 것입니다.

하얀 도화지에 그림(렌더링)을 그린다고 보시면 이해가 편합니다.

 

HTML

 

<html>
  <head>
    <title>SPA</title>
  </head>
  <body>
    <main class="app">
    </main>
    <script type="module" src="./src/app.js"></script>
  </body>
</html>

우선 기본 base html입니다. 

우리는 기본 html에 JS로 동적으로 화면을 그려줄 것 입니다.

 

(*type="module"을 쓰면 global scope가 아니고 독립적인 module scope가 생기고, ES6 module(import, export)를 사용 가능합니다.) 

 

간단히 렌더링 되기 이전까지의 과정을 요약해보자면

 

1. www.홈페이지 요청

2. 서버에서 리소스(HTML, JS, CSS)등을 클라이언트에 보내줌

3. HTML,CSS, JS 다운로드 완료

4. JS 동작 

5. HTML에 렌더링

 

BaseComponent.js

 

A,B,C 페이지 javascript폴더에 공통적으로 쓸 부모 클래스를 정의해줍니다.

 

아래에서 쓰이는 예시는 템플릿 메소드 패턴이라고 디자인 패턴을 사용한 것인데

부모 클래스에서 기본적인 로직을 짜고 아래 부분(자식 클래스)에서 구체화하는식으로 동작하게 구현하는 

방식입니다.

 

(예시는 객체지향 패러다임을 썼지만 객체지향적인 컨셉으로 짜도 되고 함수 컨셉으로 짜도 됩니다.)

export default class BaseComponent {

    constructor({$parent, setState, state}){
        this.$parent = $parent;
        this.state = state;
        this.setState = setState;
    }
    
    template(){
        return '';
    }

    #render(){
        const template = this.template();
        this.$parent.insertAdjacentHTML('beforeend', template);
    }

    setEvent(){

    }

    renderSequence(state){
        this.state = state;
        this.#render();
        this.setEvent();
    }
}

constructor에서 주입받는건 3가지인데 

이 중 $parent는 HTML의  <main class="app"></main> 값을 가지고 있는 변수(querySelector)입니다.

setState, state 두가지는 이 후에 차차 알아봅시다.

 

저런식으로 구현한다면 만약 class="app"을 가진 root element가 아니라

다른 element값을 주입해서 그 아래에 렌더링되는식으로 부모클래스가 자식 클래스를 제어할 수 있는 유연성을 가질 수 있습니다.

 

이것을 유식한 말로 DI(Dependency Injection)이라 합니다.

 

상속받은 자식 클래스들 (A.js, B.js, C.js)

 

우리가 자식 클래스에서 구현할것은 

template() 함수 와 setEvent() 함수입니다.

 

template() 함수는 안에 넣을 뼈대(string)를,

setEvent()는 렌더링 이후 페이지에 붙여줄 이벤트만 작성해주면 됩니다. (간단 예시니 event는 붙이지 않았습니다)

 

그럼 부모 클래스가 로직을 알아서 제어해서 렌더링 시켜줄 것입니다.

 

>> 자식 클래스들 코드 

더보기
import BaseComponent from '../BaseComponent.js';

export default class A extends BaseComponent{

    template(){
        return `IM A!
        ${RouterButton()}
        `;
    }

    setEvent(){

    }
}

export default class B extends BaseComponent{


    template(){
        return `IM B!
        ${RouterButton()}
        `;
    }


    setEvent(){
    }
}
export default class B extends BaseComponent{



    template(){
        return `IM B!
        ${RouterButton()}
        `;
    }


    setEvent(){
    }
}

export default function RouterButton(){
    return `
        <button type="button" class="A">
            A
        </button>
        <button type="button" class="B">
            B
        </button>
        <button type="button" class="C">
            C
        </button>
    `
};

 

이제 SpaRouter를 작성해 봅시다.

 

import A from './pages/A.js';
import B from './pages/B.js';
import C from './pages/C.js';

class app{

    constructor(){
        this.state = { 'locate' : window.location.pathname};
        this.root = document.querySelector('.app');
        
        const ObjectForDI = {$parent:this.root, setState : this.setState.bind(this), state : this.state};

        this.A = new A(ObjectForDI);
        this.B = new B(ObjectForDI);
        this.C = new C(ObjectForDI);

        this.render();
        this.setDummyEvent();
    }

    setState (newState){
        const { locate } = this.state;
        this.state = {...this.state, ...newState};
        this.render();
    }
    
    render(){
        this.root.innerHTML = '';

        let { locate } = this.state;

        console.log(locate);

        if(locate === '/A'){
            this.A.renderSequence(this.state);
        } 
        else if(locate === '/B'){
            this.B.renderSequence(this.state);
        }
        else{
            this.C.renderSequence(this.state);
        }

        this.historyRouterPush(locate);
    }

    historyRouterPush(locate) {
        window.history.pushState({}, locate, window.location.origin + locate);
    }
}

new app();

 

코드를 나눠서 살펴 봅시다.

 

라우팅 처리 방식 

 

    render(){
        this.root.innerHTML = '';

        let { locate } = this.state;

        console.log(locate);

        if(locate === '/A'){
            this.A.renderSequence(this.state);
        } 
        else if(locate === '/B'){
            this.B.renderSequence(this.state);
        }
        else{
            this.C.renderSequence(this.state);
        }
    }

지금 페이지의 경로(window.location.pathname) 에 따라 A, B, C 페이지가 라우팅됩니다.

경로가 /A면 A 페이지, /B면 B 페이지, 그외엔 C 페이지가 렌더링 되고 있습니다.

 

 

경로 변경 방식

 

    setState (newState){
        // newState = {locate : 'newPath' }
        this.state = {...this.state, ...newState};
        this.render();
    }

 

 특정 이벤트가 일어나면 setState를 호출하고, 다시 렌더링되게(위에서 보았던 render 함수 호출)됩니다.

 

그리고 state와 setState는 아까 BaseComponent.js의 생성자에서 본 state와 setState의 정체입니다.

부모 클래스(app.js)에서 자식 클래스(BaseCompoent 상속받은 A.js, B.js 등)에게 함수와 공통 객체를 주입해주고,

페이지를 바꿀 (정보 갱신) 타이밍이 오면 setState 함수를 자식 클래스에서 호출해서 해당 페이지를 전부 rerender 하게 됩니다.

 

리엑트에서 상태 변경때 쓰는 방식과 비슷하지 않나요?

내부 구현은 매우 다를거라 예상되지만... 비슷하게 작동합니다.

(이 코드에서 엄밀히 따지면 setState보다는 setRouterPath 등 다른 이름이 어울릴꺼 같긴 해요) 

 

뒤로가기, 앞으로 가기 구현

 

    historyRouterPush(locate) {
        window.history.pushState({}, locate, window.location.origin + locate);
    }

브라우저 API인 히스토리 API를 사용해서 매우 쉽게 구현 가능합니다.

 

이상 간단하게 SPA 구현 방식을 알아봤습니다.

 

 

ref

 

https://devopedia.org/single-page-application

 

Single Page Application

A web application broadly consists of two things: data (content) and control (structure, styling, behaviour). In traditional applications, these are spread across multiple pages of HTML, CSS and JS files. Each page is served in HTML with links to suitable

devopedia.org

 

참고)

 

클릭 이벤트 처리방법

 

    dummyClickEvent = ({target}) => {
        if(target.classList.contains('A')){
            this.setState({...this.state, locate : '/A'});
        }
        if(target.classList.contains('B')){
            this.setState({...this.state, locate: '/B'});
        }
        if(target.classList.contains('C')){
            this.setState({...this.state, locate: '/C'});
        }
    }


    setDummyEvent(){
        this.root.addEventListener('click', this.dummyClickEvent);
    }

 

반응형
반응형

jest로 테스트를 공부해보고 있는데 몇개 안넣었는데 3~4초 이상이 걸리길래 

한번 찾아봤다

 

https://if1live.github.io/posts/escape-from-jest-jest-is-slow/

 

Jest 탈출기 - Jest는 느리다 · /usr/lib/libsora.so

개요 요새 작업하는 프로젝트에서 jest를 사용해서 유닛테스트를 돌린다. 프로젝트가 진행될수록 테스트가 점점 느려지더니 이제 유닛 테스트 한번 돌리는데 1분이 걸린다. 라이젠 붙은 좋은 컴

if1live.github.io

 

요약) 

jest는 각 VM (Executing JavaScript) - test case에서 import를 각각 하기 때문에 느리다..

 

홀리..

반응형

'Front-end' 카테고리의 다른 글

쓸만해보이는 태그, 속성들 (메모용)  (0) 2022.07.11
반응형

See the Pen Untitled by lodado (@lodado) on CodePen.

 

css으로 직접 만들어본 loading spinner

 

출처 : https://codepen.io/mandelid/pen/kNBYLJ

 

Super Simple CSS Spinner

Just recreating http://codepen.io/scottkellum/pen/tzjCK to learn how he did it....

codepen.io

반응형

'Front-end > CSS' 카테고리의 다른 글

font-family 상속 시키기  (0) 2022.11.04
css 챌린지  (0) 2022.10.05
[CSS] box-sizing  (0) 2022.07.11
[CSS] Gap  (0) 2022.06.13
CSS 애니메이션 성능 개선  (0) 2022.01.27
반응형

* 잘못된점이 있으면 피드백 주시면 감사하겠습니다!

 

예시 화면(by react-query로 제작)

 

react-query를 보면 data, loading, error 상태가 있고 로딩 화면, 에러 화면을 각각 보여줄 수 있다

리둑스와 리둑스 사가로 비슷한걸 연습겸 한번 만들어 보았다.

 

*혹시 redux-saga 가 익숙하지 않다면

https://mskims.github.io/redux-saga-in-korean/introduction/BeginnerTutorial.html

추천

 

우선 초기 constant 를 정의한다.

/* constrant.ts */

export const START_LOADING = 'loading/START_LOADING';
export const FAIL_LOADING = 'loading/FAIL_LOADING';
export const FINISH_LOADING = 'loading/FINISH_LOADING';

export const GET_ITEM = 'GET_ITEM';
export const GET_ITEMS = 'GET_ITEMS';

export const RESPONSE_STATUS = {
  REQUEST: {
    data: undefined,
    error: false,
    empty: false,
    loading: true,
  },
  FAILURE: {
    data: undefined,
    error: true,
    empty: false,
    loading: false,
  },
  SUCCESS: {
    error: false,
    loading: false,
  },
} as const;

 

RESPONSE_STATE는 각 비동기 조건(data, loading, error)때 가지고 있을 상태이다.

SUCCESS의 경우엔 fetch된 data를 가지고 있어야 하기 때문에 나중에 상태를 만들때 주입할 예정이다.

 

~LOADING constrant들은 각 조건때의 action이다.

GET_ITEM, GET_ITEMS는 각각 아이템 하나, 아이템 리스트를 받아오는 비동기 요청 action이다.

 

이제 action를 제작해보자.

// action.ts

export const LOADING_STATUS = {
  GET_ITEM,
  GET_ITEMS,
} as const;

export type LoadingStatusType = typeof LOADING_STATUS[keyof typeof LOADING_STATUS];

function getVaildRequestType(requestType: string): LoadingStatusType {
  return LOADING_STATUS[requestType];
}

export const startLoading = (requestType: string) => ({
  type: START_LOADING,
  payload: getVaildRequestType(requestType),
});

export const failLoading = (requestType: string) => ({
  type: FAIL_LOADING,
  payload: getVaildRequestType(requestType),
});

export const finishLoading = (requestType: string, response: []) => {
  return {
    type: FINISH_LOADING,
    payload: getVaildRequestType(requestType),
    response,
  };
};

 

getVaildRequestType을 쓴 이유는 action과 reducer를 추상화하고 비동기 요청때마다 기본 템플릿으로 재사용하기 위해서 저런 번거로운 방식을 사용한것이다. (requestType로 type를 보내준다.)

 

(사실 getVaildRequestType을 안쓰고 그냥 payload:requestType해도 되긴 하는데 확장성을 위해서(혹시 상태의 이름을 변경하고 싶을때 사용하라고) 넣었다)

 

 

reducer를 만약 재사용을 하지 않는다면 비동기 요청 하나하나마다 각각 reducer를 제작해야하는 번거로움이 존재한다. 

 

이것은 reducer를 보면서 자세히 설명한다.

LOADING_STATE의 역할도 reducer를 보면서 설명한다.

각 action에 넣어주는 requestType는 redux-saga에서 넣어준다

 

// reducer.ts

function setInitState() {
  const init = {};

  const { REQUEST } = RESPONSE_STATUS;
  // eslint-disable-next-line no-return-assign
  Object.keys(LOADING_STATUS).map((key) => (init[LOADING_STATUS[key]] = REQUEST));
  return init;
}

interface LoadingActionType {
  type: string;
  payload: string;
  response?: [];
}

export function asyncLoadingReducer(state = setInitState(), action: LoadingActionType) {
  const { type, payload, response } = action;
  const { REQUEST, FAILURE, SUCCESS } = RESPONSE_STATUS;

  switch (type) {
    case START_LOADING:
      return {
        ...state,
        [payload]: REQUEST,
      };

    case FAIL_LOADING:
      return {
        ...state,
        [payload]: FAILURE,
      };
    case FINISH_LOADING:
      return {
        ...state,
        [payload]: { ...SUCCESS, data: response, empty: isEmpty(response) },
        //isEmpty는 그냥 fetch된 데이터가 비었나 확인해주는 함수
      };
    default:
      return state;
  }
}

 

[payload] : ~~상태 를 사용해서 아까 보낸 getVaildRequestType의 파라미터마다 상태를 가질 수 있다. 

 

예를 들어 LOADING_STATE에 지금 GET_ITEM 과 GET_ITEMS라는 type를 지정해두고 이후 redux-saga에서 실행할때 해당 type를  requestType(payload)로 받아온다면 type마다 { data, loading, error } 결과값을 가지는 상태를 가질 수 있다!

(이게 무슨말인지 혼동된다면 아래의 redux-saga까지 보고오면 이해가 빠를것 같다)

 

GET_ITEM, GET_ITEMS 각각 type 하나마다 { START_LOADING, FAIL_LOADING, FINISH_LOADING } case 상태(RESPONSE_STATE)를 가진다. 

ex) GET_ITEMS만 ajax 요청을 받아오고 완료되었을때 상태 출력(case FINISH_LOADING), GET_ITEMS는 아직 요청을 보내지 않았다(case START_LOADING-(default)).

SUCCESS(case FINISH_LOADING)땐 아까 만든 상태에 data를 주입해서 반환해주자.

 

이제 redux-saga를 보자.

function* abstractGenerator({ ACTION, axiosRequest }) {
  yield put(startLoading(ACTION));

  try {
    const response = yield axiosRequest;
    yield put(finishLoading(ACTION, response.data));
  } catch (error) {
    yield put(failLoading(ACTION));
  }
}

function *getItemListGenerator() {
  yield abstractGenerator({ ACTION: GET_ITEMS, axiosRequest: call(getItemList) });
}

function *getItemByIdGenerator(status: { id: number; type: string }) {
  const { id } = status;
  //yield put ~~ 
  yield abstractGenerator({ ACTION: GET_ITEM, axiosRequest: call(getItemById, id) });
}

function* getItemLIstSaga() {
  yield takeLatest(GET_ITEMS, getItemListGenerator);
}

function* getItemByIdSaga() {
  yield takeLatest(GET_ITEM, getItemByIdGenerator);
}

export function* rootSaga() {
  yield all([getItemLIstSaga(), getItemByIdSaga()]);
}

 

비동기의 기본 로직을 잘 보면

function* abStractGenerator({ ACTION, axiosRequest }) {
  yield put(startLoading(ACTION));

  try {
    const response = yield axiosRequest;
    yield put(finishLoading(ACTION, response.data));
  } catch (error) {
    yield put(failLoading(ACTION));
  }
}

 

1. 데이터 fetch를 시작한다 - START_LOADING(action)

2. action(GET_ITEM, GET_ITEMS)에 따라 ajax 요청을 보내고,  - START_LOADING 

3. 데이터를 받아오는데 성공했다면 성공 action을 반환한다. - FINISH_LOADING 

4. 실패했다면 실패 action을 반환한다. - FAIL_LOADING

 

비동기 요청시 대다수의 redux-saga는 비슷한 로직으로 실행될것이다.

그럼 이 비동기 로직도 달라지는 값인 action과 ajax 함수 값만 파라미터로 주입해준다면 재사용을 할 수 있다.

 

그리고 여기서 action인 startLoading, finishLoading, failLoading에 넣는 ACTION이 아까 보았던 requestType의 정체다.

그렇게 넣으면 payload로 action이 들어가게 되고... asyncLoadingReducer에서 [payload]마다 로딩, 에러, 성공시 알맞은 상태를 반환한다.

 

이런식으로 1개의 reducer, 1개의 saga를 재사용해 n개 이상의 다른 비동기 요청을 처리할 수 있다.

 

그리고 ajax 함수들은 이런식이다.

export const getItemList = () => axios.get(API.GET.ITEMLIST); // API.GET ~ 는 주소

export const getItemById = (itemId: number) => {
  return axios.get(API.GET.ITEM, {
    params: {
      itemId,
    },
  });
};

 

예시 component) 

export function ItemBox() {
  const { asyncLoadingReducer } = useSelector((state: RootStoreType) => state);
  const disPatch = useDispatch();

  const itemRequest = asyncLoadingReducer[LOADING_STATUS[GET_ITEM]];
  const { data, empty, loading, error } = itemRequest;

  useEffect(() => {
    disPatch({ type: GET_ITEM, id: 1 });
  }, []);

  if (loading) return <div>loading</div>;
  if (empty) return <div>emptyData</div>;
  if (error) return <div>error</div>;

  return <div>{JSON.stringify(data)}</div>;
}

 

추가) 

import { createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { asyncLoadingReducer } from './reducer';
import { rootSaga } from './saga';

const configureStore = () => {
  const sagaMiddleware = createSagaMiddleware();
  const rootReducer = combineReducers({
    asyncLoadingReducer,
  });

  const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
  sagaMiddleware.run(rootSaga);
  return store;
};

const rootStore = configureStore();

export type RootStoreType = ReturnType<typeof rootStore.getState>;
export default rootStore;

 

해본 후기)

 

그냥 react-query 쓰자

 

같이 읽으면 좋은 글)

 

https://techblog.woowahan.com/6339/

 

Store에서 비동기 통신 분리하기 (feat. React Query) | 우아한형제들 기술블로그

오늘은 주문에서 사용하는 FE 프로덕트의 구조 개편을 준비하며 FE에서 사용하는 Store에 대해 개인적인 고민 및 팀원들과 검토하고 논의했던 내용을 소개합니다. 이 과정에서 생긴 여러 가지 의

techblog.woowahan.com

 

반응형

'Front-end > React' 카테고리의 다른 글

react 17(개인 공부용)  (1) 2022.06.09
React18(개인 공부 메모용)  (0) 2022.06.09
react TMI  (0) 2022.04.13
drag and drop으로 창 크기 조절  (0) 2022.03.18
redux 기본 구조  (0) 2022.02.02
반응형

 

CORS는 많이 들어봤는데

CORB란것을 '사이트 격리'에 대해 알아보다가

새로 알게 되었다.

 

참고) 사이트 격리

 

- 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.

 

출처 : https://developers.google.com/web/updates/2018/07/site-isolation

 

즉, 웹에서 cross origin의 data resource(xml, html, json 등)를 읽어와 사용하지 못하도록 브라우저에서 막는 동작이다.

 

원리 

 

CORB가 어떻게 동작하는지 알아보자.

보통 web client는 서버에 두가지의 요청을 보낸다.

 

  1. data resources such as HTML, XML, or JSON documents
  2. media resources such as images, JavaScript, CSS, or fonts

보통 1번은 SOP & CORS 설정에 따라 브라우저에 의해 blocking 되거나 보이게 된다.

(참고로 2번 media resources들은 cors에 상관없이 요청 가능하다. ex-JSONP)

 

조건 

그리고 CORB는 위 조건을 만족한다면

renderer processa cross-origin data resource를 받아와 사용하는 것을 금지한다.

(a new security feature that prevents the contents of balance.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)

예시

그럼 예시 데모를 한번 보자.

 

https://anforowicz.github.io/xsdb-demo/index.html

 

CORB demo

Demo of CORB This page demonstrates how Cross-Origin Read Blocking (CORB) works. Please see one of the following resources for more information about CORB: Repro steps to trigger CORB: Make sure that CORB is active In Chrome M68 and later CORB is active by

anforowicz.github.io

 

<button> Add 1 <img src="https://www.chromium.org/"> </button>
<button> Add 2 <img src="https://www.w3.org/.../dummy.pdf"> </button>

 

들어가서 버튼을 한번 눌러보면

CORB 차단이 뜬다.

 

위 버튼들은 data resource(text/html, application/pdf) 요청을 cross-origin 서버에 보내는 버튼들이고, CORB에 의해 막히게 된다.

그리고 CORS와 동일하게 정상적인 요청이 왔어도 (status 200) 브라우저에서 막도록 구현된 것을 확인할수 있다.

 

 

tmi)

현재 tistory 스킨에서 영어를 읽으려니 눈이 아프다 -_-;

 

 

함께 읽으면 좋은 글)

 

CORS

https://beomy.github.io/tech/browser/cors/

 

[Browser] CORS란?

이번 포스트에서는 교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)에 대해 이야기해보도록 하겠습니다. 아래 사진과 같은 에러를 보신 적이 있으셨을 수도 있습니다.

beomy.github.io

 

reference 

 

https://chromestatus.com/feature/5629709824032768

 

https://stackoverflow.com/questions/18337630/what-is-x-content-type-options-nosniff

 

https://webhack.dynu.net/?idx=20161120.001&print=friendly 

 

https://bingbingba.tistory.com/8

 

https://developers.google.com/web/updates/2018/07/site-isolation

 

Site Isolation for web developers  |  Web  |  Google Developers

Chrome 67 on desktop has a new feature called Site Isolation* enabled by default. This article explains what Site Isolation is all about, why it’s necessary, and why web developers should be aware of it.

developers.google.com

 

반응형
반응형

ES6에서 도입된 제너레이터(Generator) 함수는 이터러블을 생성하는 함수이다.

제너레이터 함수를 사용하면 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식보다 간편하게 이터러블을 구현할 수 있다.

기존 Symbol iterator 로 피보나치 수열 구현

function fibonacci(maxi) {

  let [first, second] = [0,1];

  return {
    [Symbol.iterator](){return this;},
    next() {
      [first, second] = [second, first+second];
      return {value: first, done : first>=maxi};
    }
  }
}

const Fibo = fibonacci(1000);

while(true){
    const {value, done} = Fibo.next();
    if(done) 
        break
    console.log(value);
}
/* console 결과
VM242:20 1
VM242:20 1
VM242:20 2
VM242:20 3
VM242:20 5
VM242:20 8
VM242:20 13
VM242:20 21
VM242:20 34
VM242:20 55
VM242:20 89
VM242:20 144
VM242:20 233
VM242:20 377
VM242:20 610
VM242:20 987
*/

이터레이션 프로토콜)

 

Iteration & for...of | PoiemaWeb

ES6에서 도입된 이터레이션 프로토콜(iteration protocol)은 데이터 컬렉션을 순회하기 위한 프로토콜(미리 약속된 규칙)이다. 이 프로토콜을 준수한 객체는 for...of 문으로 순회할 수 있고 Spread 문법의

poiemaweb.com

이제 이 코드를 Generator로 변환해보자.

Generator로 변환

function* counter() {
  let count = 0;
  console.log(count);
  yield count++;                  
  console.log(count);
  yield count++;                 
  console.log('done'); 
}

const cnt = counter();

cnt.next(); // {value: 0, done: false}
cnt.next(); // {value: 1, done: false}
cnt.next(); // {value: undefined, done: true}

파이썬을 해봤다면 yield 문에 대해 한번은 들어봤을것이다.

우선 제네레이터에 대해 간략히 설명하자면

 

일반 함수와 같이 함수의 코드블록을 한번에 실행하지 않고 필요할때 재시작할 수 있는 특수한 함수이다.

value 값으로 yield 값이 리턴되고, done 값으로 언제 끝날지 알려주게 된다.

 

generator를 사용하면 

다음과 같은 방식으로도 구현 가능하다.

 

function *ex(){

  let a = yield;
  let b = yield;
  yield a+b;
}

const test = ex();

test.next();
test.next(1); // 인자전달
console.log(test.next(2)); // {value: 3, done: false}

시작지점을 알려준 뒤 

인자로 generator 안에 값을 넘겨줄수도 있다.

 

그럼 fibo 수열을 다시 generator로 구현해보자.

 

 function *fibonacci(){

  let [first, second] = [0,1];
  
  while(true){
    [first, second] = [second, first+second];
    yield first;
  }
}

const test = fibonacci();

test.next(); // 1
test.next(); // 1
test.next(); // 2
test.next(); // 3
...

코드가 매우 간략해진것을 확인 가능하다.

generator를 활용해서 무한 수열(Lazy Evaluation), 비동기 처리를 쉽게 활용 가능하다.

다만 좀 더 편리한 async/await 가 생겨서 generator 만으로 비동기 처리를 하지는 않는 편이다.

 

또한 redux-saga 등 여러 라이브러리에서 사용한다.

 

reference 

 

이터레이션 프로토콜

 

Iteration & for...of | PoiemaWeb

ES6에서 도입된 이터레이션 프로토콜(iteration protocol)은 데이터 컬렉션을 순회하기 위한 프로토콜(미리 약속된 규칙)이다. 이 프로토콜을 준수한 객체는 for...of 문으로 순회할 수 있고 Spread 문법의

poiemaweb.com

 

https://poiemaweb.com/es6-generator

 

Generator | PoiemaWeb

ES6에서 도입된 제너레이터(Generator) 함수는 이터러블을 생성하는 함수이다. 제너레이터 함수를 사용하면 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식보다 간편하게 이터러블을 구

poiemaweb.com

 

반응형
반응형

원래 redux를 처음에 공부하다가 

부스트캠프에서 프로젝트할때 recoil를 도입해서 사용했었는데

recoil이 나온지 얼마 안되다보니

redux의 기본적인 틀에 대한 코드만 메모해두려고 한다.

 

redux의 핵심 개념 - flex 패턴

 

사용자와 상호작용을 하다보며 view가 가끔씩 state을 업데이트해나갈때가 있는데,

그 영향이 다른 view나 state를 연쇄적으로 업데이트해서 서로 상호작용을 일으켜 의도치 않은 

버그나 결과를 내놓는 경우가 있었다

 

즉, 공을 튀기면 어느곳으로 튕겨나갈지 모르는 연쇄적인 반응이 일어났던것이다.

그래서 연쇄적 업데이트를 방지하고 단방향 데이터 흐름인(undirectional data flow) 인 flex 패턴이 탄생했다.

redux는 flex 패턴을 활용한 유명한 라이브러리이다.

 

Flux는 단방향으로 데이터가 흐르게 하는 구조이다.

redux는 크게 4가지 파트로 이루어져 있다.

 

상태 변경 정보인 Action

Action을 Store 에 반영하는 Dispatcher

Action을 처리해서 상태를 변경시키는 reducer

상태를 저장하는 Store

 

 

view layer에서 상태를 변경하고 싶다면 정해진 Action을 dispatch에 담아 호출한다. 

그럼 Reducer에서 그 액션을 처리하고 상태를 변경하여 store에 반영한다.

 

이제 연쇄적인 양방향 데이터 흐름을 단방향 데이터 흐름으로 바꾸었고, side effect가 없는

순수함수적인 개념으로 동작한다.

 

 

 

 

그림이니 눌러도 작동 안합니다 😂

그럼 버튼을 누르면 숫자가 늘어나거나 줄어드는 간단한 화면을

코드로 구현해보자.

Action

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

 

Action 명은 그냥 unique한 식별자를 가진 이름으로 구현하면 된다.

옛날에 수학 시간에 배운 함수를 떠올려보자.

 

함수 F 에 x 를 넣으면 y가 나온다는 수학 공식을 떠올리면

항상 같은 x를 넣으면 같은 y가 나온다.

F(x)=y

 

정해진 Action과 파라미터를 dispatcher에 넣으면 (상태가 같다면) 항상 같은 결과가 나오는것을 기대할 수 있어야 한다.

즉, 함수형 패러다임 개념을 일부 가져왔다.

강제된 법칙은 아닐테지만 의도치않은 버그를 보고싶지 않다면 따르는것이 좋다.

 

function increment(value: number) {
  return { type: INCREMENT, payload: { value } };
}

function decrement(value: number) {
  return { type: DECREMENT, payload: { value } };
}

해당 방식으로 action과 payload를 미리 정의할수도 있다.

payload는 action 이외에 view에서 받고 싶은 파라미터이다.

 

간단한 수학공식으로 예를 들면

F(x, a) = x + a 라고 볼 수 있다.

 

reducer

const initState = {
  value: 0,
};

const reducer = (state = initState, action) => {
  const { type, payload } = action;
  const { value } = state;

  switch (type) {
    case INCREMENT:
      return { ...state, value: value + payload.value };
    case DECREMENT:
      return { ...state, value: value - payload.value };
    default:
      return state;
  }
};

 

reducer는 state와 action을 받아서

알맞은 action에 따라 연산을 한 후 값을 반환한다.

 

아까 설명한 F(x)=y 의 함수(F) 역할을 하는 셈이다.

state의 초기값은 매개변수로 넣은 객체값이고, 이후 리둑스 라이브러리에서 자체적으로 관리와 갱신을 해주게 된다.

그리고 switch문 내부 가독성을 위해

case문 하나 하나 함수를 구현하는것을 추천한다.

store

const store = createStore(
  combineReducers({
    reducer,
  }),
);

store는 리엑트의 '전역' 상태 state라 보면 이해가 편하다.

store는 한개만 있어야 한다. (a single source of truth)

 

만약 reducer를 너무 크게 만들지 않고 기능별로 쪼개고 싶다면 combineReducer를 활용해서 합친다.

 

dispatcher

 

import { Provider, useDispatch, useSelector } from 'react-redux';

function Test() {
  const { value } = useSelector((state) => state.reducer);
  const dispatch = useDispatch();

  return (
    <div>
      <button
        onClick={() => dispatch({ type: INCREMENT, payload: { value: 10 } })}
      >
        up 1
      </button>

      <button onClick={() => dispatch(increment(10))}>up 10</button>
      <button
        onClick={() => dispatch({ type: DECREMENT, payload: { value: 10 } })}
      >
        down
      </button>

      {value}
    </div>
  );
}

ReactDOM.render(
  <Provider store={store}>
    <Test />
  </Provider>,
  document.getElementById('root'),
);

 

dispatcher는 미리 action을 받아서 reducer를 통해 상태를 갱신한다.

함수형 컴포넌트에서 react-redux 의 useSelector, useDispatch hook을 사용하면 쉽게 구현 가능하다.

 

      <button onClick={() => dispatch(increment(10))}>up 10</button>
      <button onClick={() => dispatch({ type: DECREMENT, payload: { value: 10 } })}>
        down
      </button>

위 코드 중간에 해당 방식처럼 dispatch에 넣는 방식으로는 함수를 쓸 수도 있고, 아니면 객체 리터럴을 정해진 형식대로 그대로 넣어도 된다.

 

(무슨 방식이든 내가 기존에 구현했던 reducer와 매칭이 잘되야 한다.)

 

나는 미리 구현된 함수를 넣는 방식을 추천한다.

남이 봤을때 dispatch만 봐서 무슨 역할을 하는지 이해가 어려울 수 있는데,

잘 명시된 함수 명으로 힌트를 주는 방식으로 구현하는게 유지보수및 협업 측면에서 좋다고 생각된다.

(위 코드는 예시 코드이니 함수명이 매우 간결하다 😂)

 

그외

 

개인적으로 

action, state, reducer 등을 한 파일에 몰아넣지 말고 

각각 모듈화해서 분리(action은 action끼리, reducer는 reducer 끼리)해서 파일구조를 짜는걸 추천한다.

 

그리고 redux-toolkit 이라는 redux를 보다 쉽게 사용 가능하게 도와주는 라이브러리도 있다.

다른 라이브러리는 redux 기반으로 설명 되어 있는 글이 많아서 처음 배우고 있을때 보면 엄청 헷갈린다.

redux가 어느정도 익숙해지면 나중 생산성 향상을 위해 도입해보는거도 나쁘지 않는거 같다.

 

같이 읽어보면 좋을 글)

 

전역 상태를 왜 쓰는지 -> props drilling 해결

https://medium.com/front-end-weekly/props-drilling-in-react-js-723be80a08e5

 

Props Drilling In React.Js

What Is Props Drilling? And How To Sidestep It?

medium.com

 

기존 react는 (model이 없는) VVM 혹은 VC 패턴이라고 보는게 정확하다는 의견도 있다.

https://stackoverflow.com/questions/51506440/mvvm-architectural-pattern-for-a-reactjs-application

 

MVVM architectural pattern for a ReactJS application

I'm a semi-senior react and JavaScript developer, I've made several Universal react application. Today our CTO told me: Do you use a software architectural pattern for your application? I've no a...

stackoverflow.com

 

reference

 

https://redux.js.org/tutorials/fundamentals/part-1-overview

 

Redux Fundamentals, Part 1: Redux Overview | Redux

The official Fundamentals tutorial for Redux: learn the fundamentals of using Redux

redux.js.org

 

 

https://sihus.tistory.com/37

 

Flux Design Pattern 이해하기 (+ 단방향, 양방향 데이터 바인딩)

MVC 디자인 패턴이 아닌 Redux의 주개념이자, 리액트를 사용하면서 컴포넌트끼리 데이터를 교류할 때, 글로벌 이벤트 시스템을 설정하는 방법인 Flux 디자인패턴에 대해 알아보고 왜 facebook에서 reac

sihus.tistory.com

 

https://yoonho-devlog.tistory.com/171

 

Redux 개념 이해하기

Flux 패턴 redux는 Flux 패턴의 구현체입니다. 따라서 Flux 패턴을 먼저 이해하면 자연스럽게 Redux를 이해할 수 있습니다. facebook은 우리가 흔히 알고 있는 MVC 패턴을 사용하는 대신, Flux라는 새로운 방

yoonho-devlog.tistory.com

 

https://hanamon.kr/redux%EB%9E%80-%EB%A6%AC%EB%8D%95%EC%8A%A4-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC/

 

Redux(리덕스)란? (상태 관리 라이브러리) - 하나몬

Redux(리덕스)란? 무엇인지 부터 간단한 실습까지 (상태 관리 라이브러리 리덕스 알아보기) ⚡️ Redux(리덕스)란? Redux(리덕스)란 JavaScript(자바스트립트) 상태관리 라이브러리이다. Redux(리덕스)의

hanamon.kr

 

 

반응형

'Front-end > React' 카테고리의 다른 글

react 17(개인 공부용)  (1) 2022.06.09
React18(개인 공부 메모용)  (0) 2022.06.09
react TMI  (0) 2022.04.13
drag and drop으로 창 크기 조절  (0) 2022.03.18
redux, redux-saga로 로딩, 에러 페이지 구현  (0) 2022.02.12
반응형

이 글은 사용자 입력(input)을 받았을 때 컴포지터가 어떻게 부드러운 상호작용을 하는지에 관한 글이다

(출처는 https://developers.google.com/web/updates/2018/09/inside-browser-part4)

 

Inside look at modern web browser (part 4)  |  Web  |  Google Developers

Input event handling with the compositor thread

developers.google.com

 

사용자 입력을 받았을때

 

브라우저 관점에서 입력이란 모든 사용자의 제스처(이벤트)를 의미한다. 스크롤도 이벤트이고, 화면 터치, 마우스 포인터를 올리는거도 입력 이벤트이다.

 

 

화면 터치와 같은 사용자 제스처가 발생했을 때 가장 먼저 제스처를 수신하는 것은 브라우저 프로세스이다. 브라우저 프로세스는 제스처가 어디에서 발생했는지만 알고 있다. 탭 내부의 콘텐츠는 렌더러 프로세스가 처리해야 한다. 

 

그래서 브라우저 프로세스는 이벤트 종류와 좌표값(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?)

 

그렇다면 여기서 한가지 의문점을 가지게 된다.

이벤트 위임을 설정할때는 어떻게 될까?

 

ex)

document.body.addEventListener('touchstart', event => {  
    if (event.target === wantedNode) {
        event.preventDefault();
    }
});

이벤트 위임을 document나 root 등 최상위에 선언했을때

이벤트위임을 최상위에 선언했을경우 다음 그림과 같이 페이지가 전부 고속 스크롤 불가 영역에 속하게 된다.

 

이런 문제를 방지하기 위해 passive : true 옵션을 전달하면 메인스레드에서 이벤트를 받지만, 컴포지터가 메인 스레드의 처리를 기다리지 않고 새 프레임을 만들어도 되는 힌트를 브라우저에 주는 옵션을 줄 수 있다.

ex)

document.body.addEventListener('touchstart', event => {  
    if (event.target === wantedNode) {
        event.preventDefault();
    }
}, {passive: true});

 

JS로 가로로만 스크롤을 제한하고 싶을때 

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()가 내부에서 실행이 안될꺼라는 약속이라는데

위 코드를 실제로 실행해보면 에러가 뜬다.

 

아래 링크 참고

https://developer.mozilla.org/ko/docs/Web/API/EventTarget/addEventListener

위 코드를 복붙해서 실행했을때 나온 오류

https://stackoverflow.com/questions/37721782/what-are-passive-event-listeners

 

What are passive event listeners?

While working around to boost performance for progressive web apps, I came across a new feature Passive Event Listeners and I find it hard to understand the concept. What are Passive Event Listene...

stackoverflow.com

스텍오버플로우에 따르면

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를 사용하면 합쳐진 이벤트의 정보를 얻을 수 있다.

 

 

 

 

reference 

 

https://d2.naver.com/helloworld/6204533

https://developers.google.com/web/updates/2018/09/inside-browser-part4

 

Inside look at modern web browser (part 4)  |  Web  |  Google Developers

Input event handling with the compositor thread

developers.google.com

 

반응형
반응형

주의) 이글은 깁니다...

 

크롬에서 렌더링은 렌더러 프로세스가 담당한다.

이 글과 이전글은 GPU가 실제 클라이언트에서 어떻게 쓰이는지 원리를 알기 위해 찾아보았던 내용이다.

그 내용을 아래에 후술한다.

 

렌더러 프로세스에 관한 이전 글)

https://lodado.tistory.com/19

 

[운영체제] 크롬 웹 브라우저의 구조 (프로세스 & 쓰레드)

자바스크립트는 싱글 쓰레드이지만, 크롬 브라우저의 탭 하나하나는 각각 하나의 프로세스이고 멀티 쓰레드 환경이다. 이전에 썼던 이벤트 루프 관련 글에서 정확히 브라우저의 내부 구조가 어

lodado.tistory.com

 

www.google.com을 검색할때 크롬에서 일어나는일 

 

면접 단골 질문이긴 한데 네트워크 말고 크롬이 하는 일만 집중해서 간단히 살펴보자.

자세한 과정은 아래 링크를 추천한다.

브라우저마다 상세 구현은 다를 수 있기 때문에

'흐름' 을 파악하는 정도면 좋을 것 같다.

 

https://developers.google.com/web/updates/2018/09/inside-browser-part2?hl=ko 

 

모던 웹 브라우저 들여다보기 (파트 2)  |  Web  |  Google Developers

브라우저가 탐색 요청을 처리하는 방법을 학습하십시오.

developers.google.com

 

Brower Process 도식도

브라우저 프로세스는 주소 창, 뒤로 및 앞으로 이동 버튼을 포함한 어플리케이션의 "chrome" 부분을 제어한다.

또한 네트워크 요청 및 파일 액세스와 같은 웹 브라우저의 권한이 부여된 보이지 않는 부분을 제어한다.

 

브라우저 프로세스 내부를 보면

버튼이나 입력창을 그리는 UI 스레드,

인터넷에서 데이터를 수신하기 위해 통신 스택을 건드리는 네트워크 스레드,

파일 같은 것들에 접근하기 위한 스토리지 스레드

등을 가지고 있다. 

 

 

 

Browser Process가 내부 NetWork thread에 브라우저 탐색 과정을 수행하게 시키고, 작업이 완료되면 

Renderer Process에게 IPC를 통해 페이지 렌더를 요청한다.

이후 과정이 우리가 흔히 아는 브라우저의 렌더링 과정이다.

 

Renderer Process가 하는 일 

렌더러 프로세스는 브라우저 탭 안에서 일어나는 모든 일들을 담당한다.

렌더러 프로세스의 핵심 역할은 HTML, CSS 그리고 자바스크립트를 사용자가 인터렉션할 수 있는 웹 페이지로 만드는 것이다.

 

프로세스는 여러개의 쓰레드로 구성되어 있다.

 

Main Thread : 대부분의 코드를 처리한다. (파싱, 렌더 등)

Work Threads : 웹 워커 혹 서비스 워커를 사용할 경우 워커 스레드가 자바스크립트 코드 일부분을 처리한다.

Compositor & Raster Thread  : 렌더러 프로세스 내부에서 페이지를 효율적이며 매끄럽게 렌더하기 위해 실행된다.

compositor와 Raster thread의 역할은 후술한다.

 

브라우저의 렌더링 과정 

웹킷의 동작 과정

이것 또한 표준이 없어서... 세부 구현이 브라우저마다 다를 수 있지만 흐름 자체를 이해하자.

브라우저는 파싱  스타일  레이아웃  페인트  합성  렌더 등의 과정을 거친다. 그 후에 JS나 CSS를 통해 DOM이나 CSS에 변화가 생길 경우 reflow 혹은 repaint 등의 과정을 수행한다.

 

1) 파싱

 

HTML 파싱

 

브라우저가 HTML 을 파싱하고 읽어들이는 과정이다.

렌더러 프로세스가 탐색을 위한 커밋 메세지를 받고 HTML 데이터를 받기 시작할 때, 메인 스레드는 텍스트 문자열 (HTML)을 파싱하기 시작하고 이를 Document Object Model (DOM)으로 변환한다.

DOM은 페이지에 대한 브라우저의 내부 표현일 뿐만 아니라 웹 개발자가 자바스크립트를 통해 상호 작용할 수 있는 데이터 구조 및 API이다.

 

예측 파싱

HTML 문서에 <img> 혹은 <link>등이 있는 경우, 사전 로드 스케너는 HTML 파서에 의해 생성된 토큰들을 살짝 보고 브라우저 프로세스에 있는 네트워크 스레드에게 요청을 보낸다.

 

파싱 차단 리소스

 

파싱 과정에서 자바스크립트가 파싱을 중단시킬수도 있다.

<script> 태그를 만나면 HTML 문서를 파싱하는것을 멈추고 자바스크립트 코드를 로딩, 파싱, 실행한다.

이를 JavaScript는 파싱 차단 리소스(parser blocking resource)라고 부른다.

 

이를 방지하기 위해서는

 

  • <script>를 body 제일 아래에 놓는다 (가장 고전적인 방법)
  • 비동기로 데이터를 내려받고 다운로드가 완료되면 실행되는 async 태그를 사용한다.
  • 비동기로 데이터를 내려받고 파싱이 완료되면 실행되는 defer 태그를 사용한다. (가장 추천)
  • 리소스 우선순위화 - <link rel="preload"> 사용

가장 추천되는 방식은 head에 script + defer attribute이다.

asnyc, defer에 대해서는 아래 글 추천

 

https://guiyomi.tistory.com/101

 

Javascript - script 로딩 시 async vs defer

HTML 파일에서 외부 js 파일을 가져오는 방법은 다음과 같다. 그럼 여기서 궁금한 점이 발생한다. script 태그의 위치는 어디에 위치해야 할까? 보통 브라우저는 HTML 문서를 파싱할 때, 위에서부터

guiyomi.tistory.com

 

CSS 파싱

외부/내부의 스타일시트의 CSS를 해석해 CSSOM 트리를 구성한다.

속성을 주지 않아도 각 element들은 브라우저마다 고유의 css 속성을 가지며, 이것을 인위적으로 통일하거나

다 지우기 위해서 보통 개발할때 normalize.css나 reset.css를 사용한다.

 

렌더 차단 리소스(rendering-blocking-resource)

 

HTML, CSSOM이 완성 이전까지 브라우저는 렌더링을 멈추게 된다.

즉, css를 네트워크에서 빨리 받아오는게 중요하다.

<link rel="preload" as="script" href="super-important.js">
<link rel="preload" as="style" href="critical.css">

preload, preconnect, prefetch 등을 사용해 브라우저에게 우선순위를 알려줄 수 있다.

 

<link href="style.css"    rel="stylesheet">
<link href="style.css"    rel="stylesheet" media="all">
<link href="print.css"    rel="stylesheet" media="print">
<link href="portrait.css" rel="stylesheet" media="orientation:landscape">
<link href="other.css"    rel="stylesheet" media="min-width: 40em">

그리고 미디어 쿼리를 이용해 자신이 사용하는 미디어 타입에 걸맞는 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)

페이지를 그리는 방법

 

컴포지팅은 한 페이지의 부분들을 여러 레이어로 나누고 그 것들을 각각 레스터화하며 컴포지터 스레드에서 페이지를 합성하는 기술이다. 그리고 만약 스크롤이 발생하면, 레이어들이 이미 레스터되었기 때문에, 해야 할 것은 새로운 프레임을 합성하는 것이다.

 

 

https://developers.google.com/web/updates/2018/09/inside-browser-part3#what_is_compositing

에니메이션은 레이어들을 움직이는 동일한 방식으로 이뤄지고 새로운 프레임을 합성한다.

어떻게 웹 사이트가 여러 레이어로 나뉘는 지 개발자 도구의 Layers panel에서 볼 수 있다.

 

참고) 레스터화와 벡터화

 

레스터화는 각 요소 이미지를 컴퓨터가 이해할 수 있는 비트맵으로 변환하고, 하나의 비트맵으로 합성하는 과정이다.

https://m.blog.naver.com/reductionist101/221567932033

 

래스터, 벡터 차이

래스터(raster)와 벡터(vector)는 컴퓨터가 이미지를 저장하는 방식이며, 둘 사이에는 차이가 있다. 첫 번...

blog.naver.com

 

Layer에 대해서

 

메인 스레드는 레이아웃 트리를 순회하여 레이어 트리를 생성한다. (이 부분은 개발자 도구 성능 탭의 Update Layer Tree 참고)

 

모든 요소들에 대해 레이어를 지정할 수도 있다. 하지만 수많은 레이어를 합성하는 것은 웹페이지의 작은 부분을 매 프레임마다 새로 래스터화하는거보다 느릴 수 있다.

 

그래서 메인 쓰레드를 사용하지 않고 레스터와 컴포지트 하는 방식을 알아보자 (쉬운 말로 GPU를 써보자)

 

 

타일들을 레스터화(비트맵 생성) 시키고 GPU에 전송시키는 레스터 쓰레드들

레이어 트리가 생성되고 페인트 순서가 결정되고 나면, 메인 스레드는 컴포지터 스레드에게 정보를 커밋한다.

그러면 컴포지터 스레드가 각 레이어를 레스터라이즈(=레스터화)한다.

 

컴포지터 스레드는 레이어들을 여러 타일로 쪼개고 각 타일을 다수의 레스터 스레드에게 보낸다.

레스터 스레드들은 각 타일을 레스터화하고 그 것들을 GPU 메모리에 저장한다.

 

컴포지터 스레드는 서로 다른 레스터 스레드들에 대해 우선 순위를 정할 수 있어서 화면 안에 보이는 (혹은 가까이 있는) 것들이 먼저 레스터될 수 있다.

 

쿼드 군집으로 컴포지터 프레임을 생성해 GPU에게 IPC를 사용해 넘긴다.

 

타일들이 레스터되면, 컴포지터 스레드는 쿼드 군집(draw quads) 라 하는 타일 정보를 모아 컴포지터 프레임을 생성한다.

 

드로우 쿼드  메모리에서 타일의 위치 및 페이지 합성을 고려하여 타일을 그릴 페이지의 위치와 같은 정보를 포함합니다.
컴포지터 프레임 한 페이지의 프레임을 나타내는 쿼드 군집의 컬렉션입니다.

 

컴포지터 프레임은 IPC를 통해서 브라우저 프로세스에게 넘어간다. 

즉, 조각조각 내서 IPC로 넘긴다. 이해하면서 내 멘탈도 지금 조각조각 나고 있다.

 

 이 때, 다른 컴포지터 프레임이 브라우저 UI 변화에 따라 UI 스레드에 의해 혹은 확장 기능에 대한 다른 렌더러 프로세스들에 의해 추가될 수 있다. (GPU process는 하나니까.. 근데 이런 경우가 언제 있는지는 잘 모르겠다)

 

이러한 컴포지터 프레임들은 GPU에게 보내져 화면에 보여진다. 만약 스크롤 이벤트가 발생하면, 컴포지터 스레드는 GPU에게 보내질 다른 컴포지터 프레임을 생성한다.

 

이러한 방식의 장점은 메인 스레드의 개입 없이(reflow와 repaint 없이) 컴포지팅이 GPU에 의해 수행된다는 것이다.

 

 

현재 CSS의

  • opacity
  • translate
  • rotate
  • scale
  • will-change 

옵션이 GPU 가속을 지원한다.

그리고 전 글에도 적었지만, 무분별한 GPU 남용은 오히려 해가 된다.

보통은 잘 바뀌지 않는 값과 에니메이션을 레이어를 분리해 지정하는게 좋다. 

(The Animation Process From 1938)

이 영상에서는 배경은 그대로 두고 앞에서 움직여야 하는 전경만 별도의 셀로 만들어서 프레임을 촬영한다. 

-> 크기, 내용은 그대로고 이동(에니메이션)만 하는 element를 지정하는게 좋다

-> 작을수록 좋다

 

함께 읽어보면 좋을 글) 

 

https://docs.google.com/presentation/d/1QRchHprSPW4JVwAUVWmGmq3gmR8nLWrJmGYFzWHW9G4/edit#slide=id.g7b5233544_0274

 

Threaded GPU Rasterization

Threaded GPU Rasterization crbug.com/454500 State of GPU Rasterization (M-42)

docs.google.com

 

https://web.dev/why-speed-matters/

 

속도가 왜 중요합니까?

사용자 경험에서 속도는 중요한 역할을 합니다. 모바일 속도로 인한 지연은 실망스러울 뿐만 아니라 비즈니스 결과에도 부정적인 영향을 미칠 수 있습니다.

web.dev

 

https://web.dev/fast/

 

빠른 로드 시간

사이트 성능을 높이기 위한 기술.

web.dev

 

reference 

https://d2.naver.com/helloworld/5237120

https://d2.naver.com/helloworld/59361

https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Virtual-DOM/#_1-%E1%84%87%E1%85%B3%E1%84%85%E1%85%A1%E1%84%8B%E1%85%AE%E1%84%8C%E1%85%A5-%E1%84%85%E1%85%A9%E1%84%83%E1%85%B5%E1%86%BC-%E1%84%80%E1%85%AA%E1%84%8C%E1%85%A5%E1%86%BC

 

Vanilla Javascript로 가상돔(VirtualDOM) 만들기 | 개발자 황준일

Vanilla Javascript로 가상돔(VirtualDOM) 만들기 본 포스트는 React와 Vue에서 사용되고 있는 가상돔(VirtualDOM) 직접 만들어보는 내용이다. 그리고 이 포스트를 읽기 전에 Vanilla Javascript로 웹 컴포넌트 만들

junilhwang.github.io

 

https://developers.google.com/web/updates/2018/09/inside-browser-part3?hl=ko 

 

모던 웹 브라우저 들여다보기 (파트 3)  |  Web  |  Google Developers

브라우저 렌더링 엔진의 내부 작동 방식

developers.google.com

https://velog.io/@itssweetrain/Reflow-%EC%99%80-Repaint

 

성능 최적화와 관련해서 : reflow 와 repaint

이렇게 렌더 트리 생성이 끝나면 배치가 시작된다. 레이아웃 단계는 트리를 생성하면서 계산된 노드와 스타일을 기기의 뷰포트에서 어떤 위치에서 어떻게 보이도록 하는지 결정하는 단계입니

velog.io

 

https://d2.naver.com/helloworld/2061385

 

 

https://medium.com/@cwdoh/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%81%AC%EB%A1%AC-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%84%B1%EB%8A%A5-%EC%9D%B8%EC%9E%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-4c9e4d715638

 

프론트엔드 개발자를 위한 크롬 렌더링 성능 인자 이해하기

3년 전에 개인 블로그에 적었던 글이라 이미 보신 분들도 있으리라 생각됩니다만, 미디엄에서 이전의 기술관련 글들을 같이 관리할 겸 같이 포스팅하기로 하였습니다. :)

medium.com

 

반응형

+ Recent posts