# 2022.03

# 3/19 함수의 합성

함수의 합성이란 함 함수의 공역이, 다른 함수의 정의역과 일치하는 경우, 두 함수를 이어 하나의 함수로 만드는 연산이다.

const getPrice = (name: string) => {
  const priceList = {
    tomato: 1000,
    apple: 400,
    cherry: 2000,
  };
  return priceList[name];
};

const isExpensive = (price: number | undefined) => {
  if (price === undefined) return false;
  return price > 1500;
};

const getResult = (name: string) => {
  return isExpensive(getPrice(name));
};

getResult('cherry');
getResult('apple');

# 3/6 ESLint no-magic-numbers 매직 리터럴

매직 리터럴은 코드에 등장하는 일반적인 리터럴 값을 말한다. 가령 원의 둘레를 계산하는 함수에서 '3.1415926..'라는 숫자가 등장했을 때, 이 숫자가 원주율이라는 것을 쉽게 유추할 수 있겠지만 프로그램이 점점 고도화될수록 처음에 극명했던 것들을 잃어버릴 수 있다. 따라서 이런 숫자는 따로 올바른 변수명을 사용해서 상수로 정의 후 사용한다.

ESLint에는 no-magic-numbers 룰이 있어서 제한을 둘 수도 있다.

# 3/5 window.requestIdleCallback(), window.requestAnimationFrame()

자바스크립트 실행 최적화 (opens new window)

# window.requestIdleCallback()

이 메서드를 이용해서 브라우저의 idle 상태에 호출될 함수를 대기열에 넣을 수 있다. 브라우저에서 자바스크립트의 실행은 이벤트 루프 모델을 따른다. 기본적으로 브라우저는 이벤트가 발생하면 바로 처리할 수 있도록 idle(유휴) 상태에 머무른다.

그러다가 사용자의 인터렉션이나 타이머, data fetching에 의해 유휴 상태가 해제되면서 작업이 실행된다. 작업이 실행되면 브라우저는 작업의 실행 결과에 따른 리페인트가 완료될 때까지 기다린다. 이러한 실행 사이클로 인해서 타이머를 사용하면 수차례의 리플로우와 리페인트가 발생될 수 있다. 함수는 일반적으로 FIFO 순서로 호출된다. 하지만 timeout 옵션이 지정된 callback을 timeout 이후에 실행하도록 처리할 수 있다. 자바스크립트가 실행되는 와중에 블로킹이 되지 않도록 프레임이 끝나는 지점에 있거나, 사용자가 비활성화 상태일 때 작업을 예약한다.

가령 사용자 분석을 위해 구글 애널리틱스 이벤트 핸들러를 다는 경우 이 메서드를 써보는 것도 좋을 듯. 혹은 사용자 인터렉션에 즉각 대응할 필요가 없는 작업들인 경우에 고려해볼 수 있겠다.

/*
* @return 실행 id 리턴. window.cancelIdleCallback() 에 인자로 전달해서 콜백 실행 취소.
* @params callback: 이벤트루프가 idle 상태가 될 때 호출되어야 하는 함수.
* @params option: {timeout: 양수일 경우 timeout ms가 지날 때까지 callback이 실행되지 않았다면 성능상에 부정적인 영향을 미치더라도 다음 idle 상태에 callback이 호출 }
*/
//
const handle = window.requestIdleCallback(callback[, options])

window.cancelIdleCallback(handle);

# 예시

window.requestIdleCallback =
  window.requestIdleCallback ||
  function(cb) {
    const start = Date.now();
    return setTimeout(function() {
      cb({
        didTimeout: false,
        timeRemaining: function() {
          return Math.max(0, 50 - (Date.now() - start));
        },
      });
    }, 1);
  };

window.cancelIdleCallback =
  window.cancelIdleCallback ||
  function(id) {
    clearTimeout(id);
  };

사례 - requestIdleCallback으로 초기 렌더링 시간 14% 단축하기 (opens new window)

# window.requestAnimationFrame()

마찬가지로 이 메서드로 애니메이션을 적절하게 스케줄링하고 60fps 달성할 수 있도록 할 수 있다. 물론 setTimeout이나 setInterval을 사용해서 애니메이션을 제어할 수도 있지만, 이 경우 콜백이 프레임에서 유휴 상태에서 실행되고, 종종 프레임이 누락되어서 버벅거리는 현상이 일어날 수도 있다.

/**
 * If run as a requestAnimationFrame callback, this
 * will be run at the start of the frame.
 */
function updateScreen(time) {
  // Make visual updates here.
}

requestAnimationFrame(updateScreen);

# 예시

let start = null;
const element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';

function step(timestamp) {
  if (!start) start = timestamp;
  const progress = timestamp - start;
  element.style.left = Math.min(progress / 10, 200) + 'px';
  if (progress < 2000) {
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);

MDN (opens new window)

모든 최신 브라우저에서 사용 가능 (22.03.05 기준)

# 3/3 리액트 쿼리 라이프 사이클 | The Lifecycle of React Query

  • 'a' 쿼리 인스턴스가 mount 됨
  • 네트워크에서 데이터 fetching 하고 'a'라는 query key로 캐싱
  • 받아온 데이터는 fresh 상태에서 staleTime (default 0) 이후 stale 상태로 변경
  • 'a' 쿼리 인스턴스가 unmount 됨
  • 캐시는 cacheTime (default 5 mins) 만큼 유지되고 그 이후엔 가비지 컬렉팅
  • 만일 cacheTime이 지나기 전에 'a' 쿼리 인스턴스가 새롭게 mount되면 refetching 되고 fresh한 값을 가져오는 동안 캐시된 데이터를 보여줌

cf. cacheTime은 stateTime과 관계없이 무조건 inactive된 시점을 기준으로 캐시 데이터 삭제.

# 3/2 JSX.Element vs ReactNode vs ReactElement 차이

클래스형 컴포넌트는 render 메서드에서 ReactNode를 리턴하고, 함수형 컴포넌트는 ReactElement를 리턴한다. JSX는 바벨에 의해서 React.createElement(componenet, props, children) 함수로 트랜스파일된다. HTML처럼 생긴 문법을 ES5 문법의 자바스크립트로 변환하는 것.

<div>Hello {this.props.toWhat}</div>
<Hello toWhat="World" />
React.createElement('div', null, `Hello ${this.props.toWhat}`);
React.createElement(Hello, { toWhat: 'World' }, null);

여기서 React.createElement의 리턴 타입이 ReactElement와 JSX.Element다. JSX.Element ⊂ ReactElement ⊂ ReactNode

# ReactElement

리액트 컴포넌트가 JSON 형태로 표현된 것

interface ReactElement<
  P = any,
  T extends string | JSXElementConstructor<any> =
    | string
    | JSXElementConstructor<any>
> {
  type: T;
  props: P;
  key: Key | null;
}

# ReactNode

ReactElement의 superset.

type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode =
  | ReactChild
  | ReactFragment
  | ReactPortal
  | boolean
  | null
  | undefined;

# JSX.Element

ReactElement의 특정 타입

declare global {
  namespace JSX {
    interface Element extends React.ReactElement<any, any> {}
  }
}

참고 (opens new window)

# 3/1 getBoundingClientRect() vs offsetTop

viewport MDN - getBoundingClientRect (opens new window)

일반적으로 offsetTop이 getBoundingClientRect() 계산보다 빠름. 하지만 offsetTop 또한 reflow를 발생시킬 수 있다. offsetTop을 읽는 과정에서 브라우저가 렌더링 큐에 쌓인 작업들을 모두 수행하게 되면서 reflow가 발생하게 되는 것. 따라서 아래와 같이 주의해서 사용한다면 보다 성능을 최적화할 수 있다.

// bad
function resizeAllParagraphsToMatchBlockWidth() {
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}

// good
const width = box.offsetWidth;
function resizeAllParagraphsToMatchBlockWidth() {
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = width + 'px';
  }
}

What forces layout / reflow (opens new window)

Last Updated: a year ago