2019-08-12 TIL

2019-08-12

React tips

Dan Abramov의 블로그를 읽다가 알게된 것들 간단히 갈무리

잘시간이라 번역과 정리는 다음에!! > 번역끝

쏟아놓고 보니 FAQ내용이 대부분인거같넹

How to create expensive objects lazily?

useMemo lets you memoize an expensive calculation if the dependencies are the same. However, it only serves as a hint, and doesn’t guarantee the computation won’t re-run. But sometimes you need to be sure an object is only created once.

useMemo는 dependency가 같다면 비용이 많이 드는 계산을 memoize하도록 해준다. 그러나, 힌트 정도이고 계산이 재 실행 되지 않을 것을 보장하지는 않는다. 하지만 가끔은 오브젝트가 확실히 단 한번만 생성되도록 만들 필요가 있다.

The first common use case is when creating the initial state is expensive:

첫 번째 용례는 비용이 많이 드는 초기 state를 만드는 것이다:

function Table(props) {
  // ⚠️ createRows() is called on every render
  const [rows, setRows] = useState(createRows(props.count));
  // ...
}

To avoid re-creating the ignored initial state, we can pass a function to useState:

초기 state의 재생성을 막기 위해 useState함수를 전달할 수 있다.

function Table(props) {
  // ✅ createRows() 는 단 한번만 호출된다
  const [rows, setRows] = useState(() => createRows(props.count)); // createRows()의 리턴값이 rows의 초기값이 된다
  // ...
}

React will only call this function during the first render. See the useState API reference.

리액트는 이 함수를 첫 번째 렌더 중에만 호출할 것이다. useState API reference를 참고.

You might also occasionally want to avoid re-creating the useRef() initial value. For example, maybe you want to ensure some imperative class instance only gets created once:

여러분은 아주 가끔 useRef() 초기값의 재생성을 피하고 싶을 것이다. 예를 들어, 어떤 명령형 클래스 인스턴스가 확실히 단 한번만 생성되도록 하고 싶을 것이다.

function Image(props) {
  // ⚠️ IntersectionObserver는 매 렌더마다 생성된다
  const ref = useRef(new IntersectionObserver(onIntersect));
  // ...
}

useRef does not accept a special function overload like useState. Instead, you can write your own function that creates and sets it lazily:

useRefuseState와 달리 특별한 함수를 받지 않는다. 대신, 생성과 lazy하게 set하는 함수를 작성할 수 있다.

function Image(props) {
  const ref = useRef(null);

  // ✅ IntersectionObserver is created lazily once
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }

  // When you need it, call getObserver()
  // ...
}

This avoids creating an expensive object until it’s truly needed for the first time. If you use Flow or TypeScript, you can also give getObserver() a non-nullable type for convenience.

이렇게 하면 정말 필요한 첫 순간까지 비용이 많이드는 오브젝트의 생성을 피할 수 있다. Flow나 TypeScript를 사용한다면, 편의를 위해 getObserver()를 null을 허용하지 않는 타입으로 설정할 수도 있다.

Is there something like instance variables?

Yes! The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class.

useRef()훅을 이용해 기존 클래스 컴포넌트의 인스턴스 변수처럼 렌더간 값이 보존되는 변수를 만들 수 있음. “ref”오브젝트는 제네릭한 컨테이너이며 그 안의 current라는 속성은 mutable하고 어떤값이든 담을 수 있음.

You can write to it from inside useEffect:

useEffect안에서 다음과같이 사용할 수 있음 :

function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });

  // ...
}

If we just wanted to set an interval, we wouldn’t need the ref (id could be local to the effect), but it’s useful if we want to clear the interval from an event handler:

만약 우리가 interval을 설정만 할거였다면 ref가 필요 없었을것임(id는 이 effect의 로컬변수). 하지만 만약 우리가 이벤트 핸들러에서 interval을 clear하도록 할 때 유용함.

  // ...
  function handleCancelClick() {
    clearInterval(intervalRef.current);
  }
  // ...

Conceptually, you can think of refs as similar to instance variables in a class. Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.

개념상, ref들을 클래스의 인스턴스 변수와 유사하다고 생각해도 된다. lazy initialization을 하는게 아니라면, ref들을 렌더링 중에 설정하지 않도록 하길 바란다. 놀라운 결과로 이어질 수 있음. 대신에, 이벤트 핸들러나 effects 안에서 ref들을 변경하길.

Should I use one or many state variables?

If you’re coming from classes, you might be tempted to always call useState() once and put all state into a single object. You can do it if you’d like. Here is an example of a component that follows the mouse movement. We keep its position and size in the local state:

기존의 state와 setState처럼 여러 state들을 한 오브젝트에 선언 해 관리할 수도 있다. (물론 state를 merge하지는 않는다)

function Box() {
  const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
  // ...
}

Now let’s say we want to write some logic that changes left and top when the user moves their mouse. Note how we have to merge these fields into the previous state object manually:

마우스 이벤트에 따라 left, top state를 변경해보자. state를 merge하도록 하는 방법에 주목하자.

  // ...
  useEffect(() => {
    function handleWindowMouseMove(e) {
      // Spreading "...state" ensures we don't "lose" width and height
      setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
    }
    // Note: this implementation is a bit simplified
    window.addEventListener('mousemove', handleWindowMouseMove);
    return () => window.removeEventListener('mousemove', handleWindowMouseMove);
  }, []);
  // ...

(함수 형태로 setState를 하고있고, 이전 state를 spread해서 보존시키고 있다.)

This is because when we update a state variable, we replace its value. This is different from this.setState in a class, which merges the updated fields into the object.

훅으로 선언한 setState는 state를 교체한다. merge하지 않는다.

If you miss automatic merging, you can write a custom useLegacyState Hook that merges object state updates. However, instead we recommend to split state into multiple state variables based on which values tend to change together.

만약 자동 merge되는 이전 방식이 그리우면 직접 useLegacyState같은 이름으로 커스텀 훅을 작성할 수 있다. 그러나, 함께 변하는 state들 끼리 묶어서 여러개의 state 변수로 나누는 것을 추천한다.

For example, we could split our component state into position and size objects, and always replace the position with no need for merging:

예를 들어, 컴포넌트의 state를 positionsize 오브젝트로 나누고, merge필요성이 없는 position을 항상 교체하도록 할 수 있다.

function Box() {
  const [position, setPosition] = useState({ left: 0, top: 0 });
  const [size, setSize] = useState({ width: 100, height: 100 });

  useEffect(() => {
    function handleWindowMouseMove(e) {
      setPosition({ left: e.pageX, top: e.pageY });
    }
    // ...

Separating independent state variables also has another benefit. It makes it easy to later extract some related logic into a custom Hook, for example:

독립적인 state 값들을 분리하는 것은 또다른 이득이 있다. 그렇게 하면 나중에 커스텀 훅으로 관련된 로직들을 추출하는 것이 쉬워진다. 예를 들면:

function Box() {
  const position = useWindowPosition();
  const [size, setSize] = useState({ width: 100, height: 100 });
  // ...
}

function useWindowPosition() {
  const [position, setPosition] = useState({ left: 0, top: 0 });
  useEffect(() => {
    // ...
  }, []);
  return position;
}

(관련된 값들 끼리 묶어놨기 때문에 따로 custom hooks로 빼낼 수 있다)

Note how we were able to move the useState call for the position state variable and the related effect into a custom Hook without changing their code. If all state was in a single object, extracting it would be more difficult.

useState 호출과 effect들을 커스텀 훅으로 코드 변경 없이 빼낼 수 있던 것에 주목하길 바란다. 만약 스테이트가 하나의 오브젝트로 되어있었다면 이렇게 추출해내는게 더 어려웠을 것임.

Both putting all state in a single useState call, and having a useState call per each field can work. Components tend to be most readable when you find a balance between these two extremes, and group related state into a few independent state variables. If the state logic becomes complex, we recommend managing it with a reducer or a custom Hook.

모든 스테이트를 한 번의 useState 호출에 집어넣는 방법과, 모든 필드에 각각 useState호출하는 방법 둘 다 작동한다. 다만 두 양 극단 사이의 적절한 균형을 맞출 때 컴포넌트의 가독성이 가장 좋아진다. 관련있는 state들을 그룹화 해 독립적인 state 변수로 만드는 것이 좋다. 만약 state 로직이 복잡해지면 reducer로 관리하거나 커스텀 훅을 사용하길 추천한다.

함수형 컴포넌트와 클래스, 어떤 차이가 존재할까?” 중..

ref는 고정된 값이 아니기 때문이 렌더링 도중에 읽거나 쓰는 것은 피하는 것이 좋다. 렌더링 내에서는 예측 가능한 일들만 일어나는 것이 권장되기 때문이다. 하지만 특정 prop과 state의 최신값을 불러오고 싶을 때마다 ref를 수동으로 처리하는 것은 내키지 않는다. 다행히 Hooks의 effect를 이용해 이를 자동화할 수 있다:

function MessageThread() {
  const [message, setMessage] = useState('');

  // 최신값을 쫓아간다  const latestMessage = useRef('');  useEffect(() => {    latestMessage.current = message;  });
  const showMessage = () => {
    alert('You said: ' + latestMessage.current);  };

(데모를 통해 확인해보자.)

effect 함수 내부에 DOM이 업데이트될 때마다 ref 값이 변하도록 설정해줬다. 이렇게 하면 인터럽트 가능한 렌더링에 의존적인 Time Slicing and Suspense과 같은 기능들이 값 변경에 의해 피해를 받지 않도록 할 수 있다.

ref를 꼭 사용해야 하는 경우는 많지않다. 될 수 있으면 props나 state를 고정시키는 것이 좋다. 하지만 interval이나 subscription 같은명령형 API다룰 때는 ref가 유용하게 쓰일 수 있다. prop, state, 심지어 함수까지 어떤 값이던 고정시켜둘 수 있다는 것을 기억하자.

이 패턴은 최적화에도 적합하다(useCallback이 자주 바뀐다던지 할 때). 하지만 이럴 때는 reducer를 쓰는 것이 조금 더 나은 해결책일 수도 있다. (추후 다룰 예정)


Minchang Kim
Minchang Kim
웹/앱 개발자 김민창입니다! 좋은 하루 되세요!