반응형

React Hook 에 대해 공부하다보면, 아래와 같은 문구를 만나게 된다.

 

 

막연하게 Hook 의 실행 순서와 선언 범위에 대한 규칙만 서술되어 있고, 왜 하지 말아야 하는지에 대한 내용은 서술되어 있지 않다. Hook 은 클로저와 아주 밀접한 관련이 있는데 클로저의 특징 때문에 생긴 제약사항이다.

 

외부에서 변수 접근을 하지 못 하게 만들고, 함수 자체를 리턴하게 하고 싶을 때, 아래와 같이 생각해볼 수 있다.

 

function getAdd() {
  let foo = 1
  return function () {
    foo += 1
    return foo
  }
}

 

함수 자체를 리턴하고 함수 내에서 사용하는 변수를 내부에 선언한다. 이렇게 구현하면 getAdd 를 통해 생성된 함수 객체를 호출할 때마다 내부에 선언되어 있는 변수에 계산을 할 수 있게 된다. 클로저다.

 

이 함수를 모듈 패턴을 통해 아래와 같이 구성할 수도 있다.

 

const add = (function getAdd() {
  let foo = 1;
  return function () {
    foo += 1;
    return foo;
  }
})()

 

사실 함수를 반환하면서 내부에 있는 변수들을 외부에서 제어하고자 한다면 고차함수들 대부분이 클로저일 수 밖에 없다.

React Hook 들도 모두 클로저이다.

 

const React = (function () {
  function useState(initVal) {
    let _val = initVal
    const state = () => _val
    const setState = (newVal) => {
      _val = newVal
    }
    return [state, setState]
  }
  return { useState }
})()

const [count, setCount] = React.useState(1)
console.log(count()) // 1
setCount(2)
console.log(count()) // 2

 

useState 를 구현해보면 위와 같다. 초기값을 인자로 받아 내부 변수에 세팅하고, 그 변수를 활용한 함수들을 리턴하는 것을 볼 수 있다. 내부 상태 값을 반환하는 함수와 갱신하는 함수를 배열 형태로 리턴하면 React 에서 흔히 사용하는 useState 와 유사하다.

 

하지만 hook 을 여러 번 사용한다면 위와 같은 구조는 정상적으로 동작하지 않게 된다. 위 React 모듈에 있는 값은 _val 하나이기 때문이다. 갱신하는 함수를 호출할 때마다 _val 변수가 덮어씌워져 호출이 되지 않는 것이다.

 

이를 방지하기 위해 배열에 hook 함수들을 배치하고 인덱스를 활용하여 사용해 접근하도록 개선했다.

 

const React = (function () {
  let hooks = []
  let idx = 0
  function useState(initVal) {
    const state = hooks[idx] || initVal
    const setState = (newVal) => {
      hooks[idx] = newVal
    }
    idx += 1
    return [state, setState]
  }
  function render(Component) {
    const C = Component()
    C.render()
    console.log(idx) // 2, 4, 6
    return C
  }
  return { useState, render }
})()

function Component() {
  const [count, setCount] = React.useState(1)
  const [text, setText] = React.useState('apple')

  return {
    render: () => console.log({ count, text }),
    click: () => setCount(count + 1),
    type: (word) => setText(word),
  }
}

var App = React.render(Component) // { count: 1, text: 'apple' }
App.click()
var App = React.render(Component) // { count: 2, text: 'apple' }
App.type('pear')
var App = React.render(Component) // { count: 'pear', text: 'apple' }

 

갱신이 정상적으로 되는 것 같지만, React.render 를 하는 순간 계속해서 idx 를 추가하게 되어서 이상하게 값이 수정되는 것을 볼 수 있다. 이미 증가된 idx 를 통해 갱신을 하는 것이 문제점이 되어 버렸다.

 

문제점을 해결하기 위해 아래와 같이 수정한다.

render 함수 내부에 idx 값을 초기화하고, 각 모듈 내의 함수에서 사용할 index 를 고정해서 공급해주면 여러 hook 들이 각자 고유한 index 를 가지게 되어 그에 맞는 상태를 변경할 수 있게 된다.

 

const React = (function () {
  let hooks = []
  let idx = 0
  function useState(initVal) {
  	const state = hooks[idx] || initVal
  	const _idx = idx // freeze
  	const setState = (newVal) => {
    	hooks[_idx] = newVal
  	}
    idx += 1
    return [state, setState]
  }

  function render(Component) {
  	idx = 0
    const C = Component()
    C.render()
    return C
  }
  return { useState, render }
})()

function Component() {
  const [count, setCount] = React.useState(1)
  const [text, setText] = React.useState('apple')

  return {
    render: () => console.log({ count, text }),
    click: () => setCount(count + 1),
    type: (word) => setText(word),
  }
}

var App = React.render(Component) // { count: 1, text: 'apple' }
App.click()
var App = React.render(Component) // { count: 2, text: 'apple' }
App.type('pear')
var App = React.render(Component) // { count: 2, text: 'peer' }

 

[출처]

본 포스팅은 (2019년 JS 컨퍼런스에서 발표한) React Hooks 와 Closure 의 관계를 잘 설명한 동영상을 재해석하였다.

 

반응형
반응형

모든 리소스를 한 번에 로드하는 것이 아니라 사용자가 보는 뷰포트 영역에 접근했을 때 지연로딩하고 싶을 때가 있다.

그 때 사용할 수 있는 API 가 IntersectionObserver API 이다. 뷰포트 영역의 특정 교차점 부분을 진입했을 때 새로운 액션을 할 수 있다.

 

IntersectionObserver() - Web API | MDN (mozilla.org)

 

IntersectionObserver() - Web API | MDN

IntersectionObserver() 생성자는 새로운 IntersectionObserver 객체를 생성하고 반환합니다.

developer.mozilla.org

 

InsersectionObserver 생성자를 살펴보면 첫 번째 인자에는 두 번째 인자(대상 요소의 가시성 비율) 값보다 역치 값이 넘어갈 경우 감시할 옵저버 함수를 만들 수 있다.

 

특정 비율이 넘어갈 때 사진을 로드하는 함수를 만든다고 생각해보자.

 

감시할 imageRef 대상이 있는데 imageSrc 이미지 경로가 없다면, 그 때 생성자를 생성하여 감시를 시작한다.

감시하다가 교차지점에 도달하면 imageSrc 이미지 경로를 세팅한다.

 

import { useState, useRef, useEffect } from "react";

export function useLazyImageObserver({ src }) {
    const [imageSrc, setImageSrc] = useState(null);
    const imageRef = useRef(null);

    useEffect(()=>{
        let observer;
        if (imageRef && !imageSrc) {
            observer = new IntersectionObserver(([entry]) => {
                if (entry.isIntersecting) {
                    setImageSrc(src);
                    observer.unobserve(imageRef.current);
                }
            }, { threshold: [0.25] });
            observer.observe(imageRef.current);
        }
        return () => {
            observer && observer.disconnect(imageRef);
        };
    }, [imageRef, imageSrc, src]);

    return { imageSrc, imageRef };
}

 

[사용부]

import { memo } from "react";
import { useLazyImageObserver } from "../hooks/useLazyImageObserver";

export const LazyImage = memo(({src, alt})=>{
    const { imageSrc, imageRef } = useLazyImageObserver({ src });
    return (
        <img ref={imageRef} src={imageSrc} alt={alt} width='200px' height='300px'/>
    );
});

 

const urlList = [
  'https://picsum.photos/200/300?random=1',
  'https://picsum.photos/200/300?random=2',
  'https://picsum.photos/200/300?random=3',
  'https://picsum.photos/200/300?random=4',
  'https://picsum.photos/200/300?random=5',
  'https://picsum.photos/200/300?random=6',
  'https://picsum.photos/200/300?random=7',
  'https://picsum.photos/200/300?random=8',
  'https://picsum.photos/200/300?random=9',
  'https://picsum.photos/200/300?random=10',
  'https://picsum.photos/200/300?random=11',
  'https://picsum.photos/200/300?random=12',
  'https://picsum.photos/200/300?random=13',
  'https://picsum.photos/200/300?random=14',
  'https://picsum.photos/200/300?random=15',
  'https://picsum.photos/200/300?random=16',
  'https://picsum.photos/200/300?random=17',
  'https://picsum.photos/200/300?random=18',
  'https://picsum.photos/200/300?random=19',
  'https://picsum.photos/200/300?random=20',
  'https://picsum.photos/200/300?random=21',
  'https://picsum.photos/200/300?random=22',
  'https://picsum.photos/200/300?random=23',
  'https://picsum.photos/200/300?random=24',
  'https://picsum.photos/200/300?random=25',
  'https://picsum.photos/200/300?random=26',
  'https://picsum.photos/200/300?random=27',
  'https://picsum.photos/200/300?random=28',
  'https://picsum.photos/200/300?random=29',
  'https://picsum.photos/200/300?random=30',
  'https://picsum.photos/200/300?random=31',
  'https://picsum.photos/200/300?random=32',
  'https://picsum.photos/200/300?random=33',
  'https://picsum.photos/200/300?random=34',
  'https://picsum.photos/200/300?random=35',
  'https://picsum.photos/200/300?random=36',
];

function App() {
  
  return (
    <div style={{ width: '600px' }}>
      {urlList && urlList.map((url, index) => (
          <LazyImage key={index} src={url} alt=""/>
      ))}
    </div>
  );
}

 

반응형
반응형

특정 기간동안 함수가 너무 많이 호출될 경우, 함수 실행을 건너뛸 수 있는 debounce 나 throttle 기법을 고려할 수 있다.

그 중 debounce 는 이벤트를 그룹화하여 많은 이벤트가 발생해도 모두 무시하고, 하나의 이벤트만 실행시키도록 하는 기법이다. lodash 라이브러리에서 지원한다.

 

input 태그에서 사용자가 입력을 수시로 변할 때, 백엔드에 input 데이터를 보내는 예제를 생각해보자.

 

const Input = () => {
  const [value, setValue] = useState();

  const sendRequest = (value) => {
    // 백엔드에 input 데이터를 보냄
  };

  const debouncedSendRequest = debounce(sendRequest, 500);

  const onChange = (e) => {
    const value = e.target.value;
    setValue(value);
    debouncedSendRequest(value);
  }

  return <input onChange={onChange} value={value} />
}

 

위와 같이 코드를 생각해볼 수 있다. onChange 함수에서 value 를 세팅하고 debounced 된 함수를 호출하는 형태이다.

하지만 위 코드는  value 값이 바뀔 때마다 리렌더링이 되면서 아래와 같은 문제점이 생긴다.

 

  • sendRequest, debouncedSendRequest 함수가 계속 파괴되고 생성된다.
  • timer 로 인해 바로 파괴되는 것이 아니라 timer 시간 동안 유지되었다가 참조하는 곳이 없어서 가비지 컬렉션에 의해 정리된다.

 

const Input = () => {
  const [value, setValue] = useState("initial");

  const sendRequest = useCallback((value) => {
    // 백엔드에 input 데이터를 보냄
  }, []);

  const debouncedSendRequest = useMemo(() => {
  	return debounce(sendRequest, 500);
  }, [sendRequest]);

  const onChange = (e) => {
    const value = e.target.value;
    setValue(value);
    debouncedSendRequest(value);
  }

  return <input onChange={onChange} value={value} />
}

 

리렌더링이 되어서 함수가 파괴가 되어 참조를 못 하는 문제를 해결하기 위해 sendRequest 함수 자체에는 useCallback 훅으로 감싸고, debouncedSendRequest 함수는 useMemo 훅으로 감싸서 코드를 수정해보았다. 정상적으로 동작하는 것처럼 보인다.

 

debouncedSendRequest(value) 구문보면 계속해서 value 인자를 주고 있는데 이 부분을 useCallback 의 종속성 인자로 바꾸어 보면, 결국 처음 코드와 같게 된다.

 

리바운싱할 때마다 디바운스 함수를 생성하지 않게 하는 것이 관건인데 보통 useRef 훅으로 함수를 감싸는 것을 추천한다. 아래와 같은 형태가 되는데 만약 useRef 내부 함수에서 state 값을 참조하면 클로저가 되어버리기 때문에 주의해야 한다.

 

const ref = useRef(debounce(() => {
    // value 가 scope 를 벗어난 외부 변수이기 때문에 초기값으로 세팅된다 (클로저)
    console.log(value);
}, 500));

 

value 상태 값을 항상 최신으로 유지하기 위해 함수를 다시 호출하고 ref 에 다시 할당해야 한다.

(즉, 클로저라는 문제 때문에 value 값이 변경될 때마다, ref.current 값을 교체해주어야 된다.)

 

useEffect(() => {
    ref.current = debounce(() => {
    }, 500);
}, [value]);

 

또 처음 코드와 같게 된다. 클로저로 변형된 함수들을 Ref 로 묶고, debouncedCallback 함수만 useMemo  로 결과 값만 재 사용한다면 위 useMemo 와 useCallback 의 이점을 동시에 사용할 수 있게 된다.

 

[최종 코드]

  const [value, setValue] = useState("initial");
  const ref = useRef();

  const onChange = () => {
  };

  useEffect(() => {
    ref.current = onChange;
  }, [onChange]);

  const debouncedCallback = useMemo(() => {
    const func = () => {
      ref.current?.();
    };
    return debounce(func, 1000);
  }, []);

 

[출처]

How to debounce and throttle in React without losing your mind (developerway.com)

반응형
반응형

커링은 하나 이상의 매개변수를 가진 함수를 단일 인수를 갖는 함수들의 함수열로 바꾸는 기법을 말한다.

이 커링 기법을 사용하면, 인자들을 지금 현재 다 받지 않아도 함수 형태로 계속 전달할 수 있고, 인자들을 쪼갤 수 있어 공통 인자가 있는 함수들을 그 인자를 생략한 채 사용할 수 있다는 장점이 있다.

 

common => func1(a, common);

common => func2(b, common);

common => func3(c, common);

 

이렇게 세 함수가 같은 인자를 받아 실행하는 함수라면 커링을 통해 common 을 생략하고 바로 부를 수 있다.

 

func1(a);

func2(b);

func3(c);

 

이외에도 인자의 갯수가 줄어 unit test 가 편리하고 가독성이 높아진다는 장점이 있다.

 

커링 함수를 자바스크립트 버전으로 표현하면 다음과 같다.

 

const curry = func => function curried(...args) {
    if (args.length >= func.length) return func.apply(this, args);
    return (...args2) => curried.apply(this, args.concat(args2));
};

 

간단하다.

원래 함수의 인자가 모두 들어오기 전까지는 지금까지의 인자들을 더해 Wrapper 함수로 전달 후 Wrapper 함수를 리턴하고 모든 인자가 들어오면 원래 함수를 호출한다.

반응형

'언어 > Javascript' 카테고리의 다른 글

[JS] 일급함수  (0) 2023.02.10
[JS] 비동기 클린 코드과 에러 핸들링  (0) 2023.02.02
반응형

일급함수

자바스크립트의 함수는 일급함수이다. (함수를 값으로 다룰 수 있다라는 말과 동치)

 

일급객체

변수에 할당할 수 있고, 함수의 인자나 함수의 리턴값으로 사용될 수 있는 객체를 일급객체라고 말한다.
즉 객체를 값으로 다룰 수 있다.

 

일급함수는 함수가 다른 일급객체(변수)와 동일하게 다루어 질 수 있다는 말이다. 즉 함수가 변수로 사용될 수 있다.

 

// 함수의 결과값으로 함수를 사용할 수 있다.
const returnFunc = () => () => 1;
console.log(returnFunc) // 함수의 인자로 함수가 사용된 모습이다.
console.log(returnFunc()); // () => 1; 함수가 출력됨

// 만들어진 함수를 변수로 담을 수 있다.
const wrapper = returnFunc();
console.log(wrapper); // () => 1; 함수가 출력됨

 

이렇게 함수가 변수로 사용되면 아래와 같은 장점을 가지게 된다.

 

1. 즉시 실행되지 않아도 된다. 지연 실행을 지원한다.

2. 함수의 합성을 통해 고차 함수(함수를 인자로 받아서 실행하는 함수), 클로저 등을 만들 수 있다.

 

const apply = f => f(1);
const add10 = a => a+10;

console.log(apply(add10)); // 함수의 합성~! 11
console.log(apply(a => a - 1)); // 직접 함수를 인자로 전달하여 함수를 합성할 수도 있다. 0

 

클로저란, 함수 내부에서 외부 변수를 기억하고 있는 구조를 클로저라고 한다.
(인수와 함수 모두를 통틀어 클로저라고 부른다.)

 

// 클로저
const addMaker = a => b => a + b;
const add10 = addMaker(10);

log(add10(5)); // 15
log(add10(10)); // 20

 

함수가 조합과 추상의 도구로 사용될 수 있고 고차 함수를 통해 함수형 프로그래밍이 가능해진다. 다른 OOP 언어에서는 자바 언어의 함수형 인터페이스(Supplier, Predicate, Consumer, Function) 들이 대표적인 예가 될 수 있다.

반응형

'언어 > Javascript' 카테고리의 다른 글

[JS] 커링함수  (0) 2023.02.12
[JS] 비동기 클린 코드과 에러 핸들링  (0) 2023.02.02
반응형
오늘의 공부 주제!

 

요구사항 1 : 이미지의 height 값을 계산하자.

f1 함수에서 height 값을 계산하도록 img.height 형태로 로직을 수정해보면, 정확한 값이 계산되지 않는다. 0 이 출력된다.

url 속성과 다르게 height 속성은 객체가 로드가 될 때 제대로 계산이 될 수 있기 때문이다. onLoad 속성의 이벤트(오브젝트가 로드되었을 때 발생하는 이벤트)를 사용해서 객체가 로드될 때 계산이 되도록 변경한다.

 

=>

const loadImage = url => {
    let img = new Image();
    img.src = url;
    img.onload = () => {
        console.log(img.height);
    }
    return img;
};
loadImage(imgs[0].url);

 

요구사항 2 : 이미지의 height 값을 외부로 반환하는 함수로 만들자.

onLoad 속성은 이벤트다. 오브젝트가 로드되었을 때 발생하는 이벤트로 내부 표현식에 있는 변수들을 외부로 반환하기 위해서는 onLoad 이벤트가 발생할 때까지 기다려야 한다. 순차적으로 실행되지 않고 어떤 특정 시점에 호출되는 함수 내부에 있는 변수들을 반환해야 하므로 Promise 객체를 반환하도록 변경한다.

 

=> 

const loadImage = url => new Promise(resolve => {
    let img = new Image();
    img.src = url;
    img.onload = () => {
    	resolve(img);
	}
});
// loadImage(imgs[0].url).then(img => console.log(img.height));

 

요구사항 3 : 이미지 리스트들을 순회하면서 height 값을 출력하자.

image url 이 주어졌을 때 height 값을 구하는 함수를 만들었으니 리스트들을 순회하면서 map 으로 해당 함수를 호출하여 일대일 변환해보자.

 

=>

// 실패코드
function f() {
    imgs
    .map(({url}) => loadImage(url))
    .map(img => img.height)
    .forEach(a => console.log(a));
}

 

이렇게 구현하면 loadImage 가 Promise 를 반환하기 때문에 다음 구문들도 Promise 를 인자로 받아 실행이 된다.

이 때 Promise 객체에서는 없는 속성(여기서는 height 값) 을 참조하므로 undefined 가 최종적으로 반환된다. 결과 값을 기다려야 하므로 loadImage 함수를 await 지시어를 사용해 기다리고, await 을 사용했으니 function 에는 async 가 붙는다.

 

그런데 이 async 함수에서 반환하는 값은 또 Promise 이므로 다음 구문에서도 계속해서 Promise 를 사용하기 위해 async-await 구문을 사용해야 한다. async-await 구문 지옥. 일단 해당 함수를 패치해서 기능 구현은 정상적으로 해두었다.

 

=>

function f() {
	imgs
    .map(async ({url}) => {
        const img = await loadImage(url);
        return img.height;
    })
    .forEach(async a => console.log(await a));
}

 

요구사항 4 : 이미지 리스트들을 순회하면서 총 height 값을 구해보자.

이미지 리스트들의 height 값을 더해 총 height 값을 구현해야 하는 요구사항이 추가되었다. 단순히 출력하는 코드를 수정해 reduce 기능을 활용한다.

 

=>

async function f() {
    const total = await imgs
        .map(async ({url}) => {
            const img = await loadImage(url);
            return img.height;
        })
        .reduce(async (total, height) => await total + await height, 0);
	console.log(total);
}

 

더 심각해졌다. 내부에 있는 async-await 구문들이 외부까지 전파가 된 모습이다. 합계를 출력해야 되기 때문에 함수 내부에 total 변수를 생성하였는데 async 구문 안에서 사용하므로 await 을 통해 결과 값을 기다려주어야 한다. await 을 붙였으니 async 가 붙을테고 Promise 가 계속 전파되어버린다.

 

리팩토링 1 : 제너레이터와 코드 분리를 통해 자주 사용하는 로직을 관심사 분리한다.

리팩토링 1-1 : map 을 Promise 객체가 와도 컨트롤 할 수 있게 변경하자.

Promise 가 오면 then 과 함수를 합성해서 반환하고 일반 객체가 오면 그 객체를 함수 호출해서 그대로 반환한다. 원조 map 은 Iterable 한 객체를 인자로 받아서 가공하고 하나씩 결과 값을 방출하는 Generator 이다.(Iterator 의 특수한 형태) 새롭게 만들 map 도 Generator 형태로 만들면 된다.

 

=>

function* map(f, iter) {
    for (const a of iter) {
        yield a instanceof Promise ? a.then(f) : f(a);
    }
}

 

리팩토링 1-2 : reduce 함수도 Promise 객체를 컨트롤 할 수 있는 비동기 버전을 만든다.

인자들이 Promise 이면 계속해서 모든 인자들을 await 을 붙여 신경 써주어야 하고 함수 자체를 async 하게 만들어야 하므로 map 과 똑같이 비동기 버전을 만들 필요가 있다. 다만, map 과 다른 점은 Promise 를 반환할 일이 없기 때문에 제너레이터 성격이 아닌 이터레이터만 만들면 되므로 굳이 then 과 합성할 필요가 없다.

 

=>

async function reduceAsync(f, acc, iter) {
    for await (const a of iter) {
        acc = f(acc, a);
    }
    return acc;
}

 

더보기

참고로, Promise.all() 과의 차이점은 다음과 같다.
Promise.all() 은 인자의 프로미스 배열을 동시에 실행한다.
for await ( ~ of ~ ) 내의 비동기 작업은 루프를 돌며 순차적으로 실행된다.

 

만들어 둔 두 가지 함수로 리팩토링하면 아래와 같이 간결해진다.

 

async function f(imgs) {
    return await reduceAsync((a, b) => a + b, 0,
                map(img => img.height,
                map(({url}) => loadImage(url), imgs)));
}

 

어차피 이 코드도 Promise 를 반환하기 때문에 async 와 await 을 없애도 된다.

 

const f = (imgs) => 
	reduceAsync((a, b) => a + b, 0,
    	map(img => img.height,
        	map(({url}) => loadImage(url), imgs)));

 

만약에 여기서 img 중 하나가 잘못된 img 라면 어떻게 될까? 어떻게 에러 핸들링을 해야 깔끔한 코드가 될까?

정답은 이 코드 바깥에서 예외처리 한 번만 해주는 것이다. 이 함수 내부에서 어떻게든 처리하려고 하면 에러가 숨게 되고 못 찾을 가능성이 있다.

 

불필요하게 에러 핸들링을 미리 해두어 에러를 숨기는 것보다 차라리 에러가 발생해야 좋은 코드이다.

(인자 값으로 들어오는 입력 값이 정확한 값이라면 절대 에러가 나지 않는 코드.) 즉 순수함수를 작성하고, 해당 순수함수를 사용하는 쪽에서 에러 핸들링을 해야 에러 핸들링도 적게 하고, 깔끔한 코드를 만들 수 있게 된다. 최대한 해당 로직을 사용하는 코드 쪽에서 최종 예외 처리 해주는 것이 좋다.

 

Q & A

1. 에러가 발생했을 때 실행이 멈추어 버리는 이유는?

Exception 이 then 함수와 만나 합성하면 reject 이 반환되고 이 reject 이 await 이라는 구문을 만나면 예외를 throw 한다.

즉, 예외 전파가 가능한 것이다.

반응형

'언어 > Javascript' 카테고리의 다른 글

[JS] 커링함수  (0) 2023.02.12
[JS] 일급함수  (0) 2023.02.10
반응형

useQuery 옵션 중에 onSuccess, onError, onSettled 같은 상태 변화가 일어날 때 트리거해주는 옵션이 있다.

 

 

현재 조회하고 있는 데이터의 상태에 따라 동작을 수행할 수 있어 onSuccess 에서 state 를 변경하곤 한다.

심지어 구글 첫 번째 검색 결과인 스택오버플로우에서도 권장하고 있는 방법이었다.

 

javascript - Store data from useQuery with useState - Stack Overflow

 

Store data from useQuery with useState

I'm using React hooks both to fetch GraphQL data with react-apollo and to store local state: const [userData, setUserData] = useState({}) const { loading, error, data } = useQuery(USER_QUERY) How...

stackoverflow.com

 

하지만 이는 안티패턴이라고 한다. Suspense 같은 React 에서 지원하는 컴포넌트를 같이 사용하게 될 때 렌더링 순서가 어떻게 바뀔지 모르기 때문이다. 

 

setState in onSuccess is not working first time with suspense · Issue #3784 · TanStack/query (github.com)

 

setState in onSuccess is not working first time with suspense · Issue #3784 · TanStack/query

Describe the bug With Suspense, calling setState in useQuery.onSuccess is not changing state. I found that this issue would be fix turning off refetchOnWindowFocus. but it still no good UX (require...

github.com

 

실제로 Suspense 와 useQuery 의 onSuccess (setState 동작) 를 같이 사용할 때 state 값이 업데이트가 되지 않는 이유를 물어보는 Tanstack/Query Issue 이다. 정리해보면 다음과 같다.

 

Suspense 와 useQuery, useEffect, onSuccess 가 호출되는 순서

1. <Suspense> 컴포넌트가 마운트 된다.
2. 자식 컴포넌트가 마운트 된다.
3. useQuery 비동기 함수가 호출이 된다. (이 때 Promise 를 던진다)
4. (<Suspense> 컴포넌트가 자식의 비동기 함수를 관찰하기 시작한다.)

Promise 의 상태가 Pending 상태이므로 <Suspense> 컴포넌트의 자식 컴포넌트가 언마운트된다.

그리고 Fallback 컴포넌트를 마운트한다.

5. 해당 시점에 useEffect 는 호출되지 않는다. 이미 언마운트 되었기 때문이다.

그러나 onSuccess 는 이 시점에 호출되고 언마운트 된 컴포넌트의 state 를 변경하려고 시도하기 때문에 아무 일도 일어나지 않는다.

6. Promise 의 상태가 Complete 되면서 특정 결과를 반환할 때 캐싱 처리된다.

7. Fallback 은 언마운트 되고 다시 자식 컴포넌트를 마운트한다.

8. 자식 컴포넌트가 마운트 되는 시점에 useEffect 가 호출되고, useQuery 캐시에 저장되어 있던 결과를 로드한다.

 

Suspense 가 결과가 회신되기 전까지 자식 컴포넌트를 언마운트하기 때문에 onSuccess 의 호출시점이 언마운트 중에 호출 되었다면 상태가 업데이트 되지 않는다는 것이다.

 

 

그래서 useQuery 로 도출한 결과 값을 useEffect 로 모니터링하고 해당 결과 값이 바뀌면 상태를 바꾸어야 한다.

반응형
반응형

Rules of Hooks – React (reactjs.org)

 

Rules of Hooks – React

A JavaScript library for building user interfaces

reactjs.org

Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)

 

위 React 공식문서를 살펴보면, 반복문 조건문 안이나 nested function 안에 hook 을 호출하면 안 된다고 명시하고 있다.

무조건 React Function Top level 에서 호출해야 한다. 이 규칙을 정해야 hook 의 호출 시점이 일관됨을 보장할 수 있기 때문이다.

 

따라서 Hook 을

 

- 조건문에서 사용하고 싶다면 결과 값을 useState 와 useEffect 에서 핸들링한다.

- 반복문에서 사용하고 싶다면 Hook 에서 반복을 지원하는지 살펴보거나, 반복할 리스트를 map 으로 맵핑하여 인자로 전달한다.

반응형

+ Recent posts