[React]Recoil 비동기 데이터 호출

Inung_92·2024년 1월 4일
1

React

목록 보기
13/15
post-thumbnail

Recoil의 상태관리란?

recoil은 애플리케이션 내에서 공유되는 상태를 효과적으로 관리하기 위해 데이터 플로우 그래프를 사용해서 상태를 정의하며, 이러한 상태들을 리액트 컴포넌트에 제공하여 컴포넌트 간의 상태 공유를 용이하게 한다.

여기서 이야기하는 데이터 플로우 그래프에 대한 설명은 다음과 같다.

상태와 그 상태를 구독하는 컴포넌트 간의 의존 관계를 시각적으로 나타낸 것을 의미

recoil의 상태는 atom이라고 불리는 단위로 정의되고, atom들은 서로 연결되어 있는 데이터 플로우 그래프를 형성하고, selector를 이용해 파생된 상태는 데이터 플로우 그래프에서 파생되는 상태를 의미한다. recoil은 이러한 의존성을 자동으로 추적하고, 상태가 업데이트 될 때마다 파생된 상태를 다시 계산해서 최신 값을 유지하는 것이다.

여기서 중요한 것은 selector를 이용해서 비동기 데이터를 데이터 플로우 그래프로 포함시키는 것이 가능하다는 점이다. selector는 순수 함수를 대표하고 있다. 즉, 주어진 인풋으로 항상 같은 결과를 만들어 낼 수 있기 때문에 DB 쿼리를 모델링하는데 좋은 방법으로 사용되는 이유이다.


비동기 데이터 호출

selector를 이용해 데이터 베이스에 저장되어 있는 데이터를 호출하기 위해서는 Promise를 반환하거나 async 함수를 사용해서 결과를 전달하면된다. 여기서 selector의 의존성에 하나라도 변경이 생기면 새로운 쿼리를 평가하고 재실행시킬 것이다. 또한, 결과는 쿼리가 이전 인풋 데이터와 다른 경우에만 실행되도록 캐싱된다.

코드(매개변수 x)

// 상태 정의
const userIdState = atom({
	key: 'userIdState',
	default: 2,
});

const currentUserNameQuery = seletor({
	key: 'currentUserNameQuery',
	get: async ({get}) => {
		const response = await getUserName({
			userId: get(userIdState),
		});
		return response.data;
	}
});

// 호출
function TestComponent () {
	const userName = useRecoilValue(currentUserNameQuery);
	
	return <div>{userName}</div>;
}

코드(매개변수 o)

// 상태 정의
const currentUserNameQuery = seletor({
	key: 'currentUserNameQuery',
	get: async (userId) => {
		const response = await getUserName({userId});
		return response.data;
	}
});

// 호출
function TestComponent ({userId}) {
	const userName = useRecoilValue(currentUserNameQuery(userId));
	
	return <div>{userName}</div>;
}

selector를 통해 비동기 데이터 호출을 위와 같이 수행했을 때 한가지 고려해야 할 부분이 있다. 리액트의 렌터 함수가 동기로 실행되는데 Promiseresolve 되기 전에 컴포넌트는 어떤 것을 렌더링 할 수 있으며, 무엇을 렌더링 해야할까? 위 코드만 실행을 시켰을 경우에는 아래와 같은 경고가 발생한다.

이런 문제점을 해결하기 위해서는 어떻게 해야할까?

Suspense

recoil은 보류중인 데이터를 다루기 위해서는 Reqct Suspense와 함께 동작하도록 디자인 되어 있다. Suspense는 하위 항목이 로드가 완료될 때까지 대체 컴포넌트를 표시할 수 있는 API이다.

다음과 같은 경우에 사용된다.

  • 콘텐츠가 로드되는 동안
  • 콘텐츠를 한번에 함께 공개
  • 로드 시 중첩된 콘텐츠 표시
  • 새로운 콘텐츠가 로드되는 동안 오래된 콘텐츠 표시
  • 이미 공개된 콘텐츠가 숨겨지는 것을 방지
  • 전환이 일어나고 있음을 나타냄

코드

function TestComponent (){
	return (
		<RecoilRoot>
			<Suspense fallback={<div>Loading...</div>}>
				<App/>
			</Suspense>
		</RecoilRoot>
	);
}

suspense는 렌더링하려는 실제 UI를 로드가 완료되지 않은 경우 fallback으로 지정된 컴포넌트로 대체하면서 데이터의 준비를 도와주는 API라고 보면 편하다.

이렇게 suspense를 사용해야 Promiseresolve된 후 데이터를 정상적으로 출력 할 수 있게 된다. 만약 suspense를 사용하는 것이 싫다면 react-query 등을 사용하여 서버 데이터를 가져온 이후 원하는 시점에 상태로 업데이트하여 사용하는 등의 방법을 사용해보자.

예시

function SomeComponent () {
  // 데이터 호출
  const someData = useQuery(getData());
  const [someState, setSomeState] = useRecoilState(someState);
  
  useEffect(() => {
    // api 데이터의 응답 값이 오면 state 업데이트
  	if(someData){
    	setSomeState(someData);
    }
  }, [someData]);
  return (
  	...생략
  );
}

Loadable

selector를 이용해 컴포넌트에서 데이터를 호출할 대 반환되는 데이터가 컴포넌트의 렌더링 이후에 발생한다면 위와 같은 에러가 발생한다. 이러한 문제점을 해결하기 위한 두번째 방법으로는 recoil의 useRecoilValueLoadable 훅을 사용하는 것이다.

useRecoilValueLoadable 훅은 loadable 객체를 반환하는데 해당 객체에는 contentsstate 상태가 포함되어 있다. 여기서 state는 다음 세가지 상태로 분류된다.

  • hasValue : 데이터 로드가 완료되어 데이터를 가지고 있는 상태
  • loading : 데이터를 로드하기 위해 진행중인 상태
  • hasError : 데이터 로딩 중 에러가 발생한 상태

위 세가지 상태를 이용하여 조건부 렌더링을 수행하면 컴포넌트가 렌더링 된 이후 데이터가 로드되어 발생하는 에러를 차단할 수 있다.

코드

// 사이드 메뉴 생성
const SideList = () => {
	// loadable 객체 호출
	const loadableDomains = useRecoilValueLoadable(sideMenuState.sideManus);
	// 상태를 변수로 대입
	const state = loadableDomains.state;
	
	return (
		<SideListBox>
			<SideUl>
				// 데이터를 가지고 있는 상태라면 컴포넌트 렌더링
				{state === "hasValue" &&
					loadableDomains.contents.map((domain) => (
						<li key={domain.name} className="side-title">
							<div className="title-box">{domain.name}</div>
							<SideCategory categories={domain.categories} />
						</li>
				))}
			</SideUl>
		</SideListBox>
	);
};

먼저 loadable 객체를 호출하고 상태를 변수로 받는다. 해당 변수의 상태 변화에 따라 렌더링 조건을 부여해주면 되는데 나는 위 코드에서 처럼 데이터를 가지고 있는 경우에만 렌더링 되도록 조건을 1개만 부여하였다. 필요하다면 로딩 상태를 나타내는 컴포넌트 등을 부여해주면 좋다.

Suspenseloadable은 특징이 다르기 때문에 사용할 때에 해당 컴포넌트가 동작하는 범위와 역할을 잘 고려하여 적용해야한다. Suspense 같은 경우는 컴포넌트를 감싸서 조금 더 넓은 범위에서 적용이 되며, loadable의 경우에는 해당 recoil 상태에 국한되어 적용되기 때문에 이 부분을 잘 고려해보자.

profile
서핑하는 개발자🏄🏽

0개의 댓글