📙

4.3 Recoil 비동기 처리

4.3.1 비동기 처리의 개념와 중요성

현대의 애플리케이션은 빠른 응답 속도와 동시에 여러 작업을 수행할 수 있는 능력을 요구합니다. 이를 가능하게 만드는 핵심 기술 중 하나가 비동기 처리입니다. 비동기 처리란 프로그램에서 작업을 요청한 후 그 결과를 기다리지 않고 다른 작업을 계속할 수 있는 프로그래밍 패턴을 말합니다. 비동기 처리를 사용하면 여러 작업을 동시에 처리함으로써 애플리케이션의 응답 시간이 단축되고 이로 인해 클라이언트는 오래 기다리지 않고 더욱 쾌적하게 애플리케이션을 사용할 수 있습니다. 또한, 필요한 순간에만 자원을 할당받아 사용하기 때문에 CPU 및 메모리 자원을 효과적으로 사용할 수 있습니다. 주로 실시간 채팅, 온라인 게임, 주식 시장 데이터와 같이 실시간으로 변화하는 데이터를 다루는 경우 비동기 처리가 필수적으로 사용됩니다.
 

4.3.2 Recoil을 통한 비동기 처리

간단한 예시를 통해 비동기 데이터를 처리하는 과정을 설명하겠습니다. 아래의 코드는 openAPI인 TheCatAPI (
)를 이용해 랜덤으로 고양이의 이미지를 보여주는 코드입니다.
import React, { useState, useEffect } from "react"; function App() { const [imageData, setImageData] = useState(null); useEffect(() => { const fetchCatImage = async () => { const url = "<https://api.thecatapi.com/v1/images/search?limit=1>"; const api_key = "DEMO_API_KEY"; try { const response = await fetch(url, { headers: { "x-api-key": api_key, }, }); const data = await response.json(); const firstImageData = data[0]; setImageData(firstImageData); } catch (error) { console.log(error); } }; fetchCatImage(); }, []); return ( <> <h1>귀여운 고양이 이미지</h1> <div> {imageData && ( <img src={imageData.url} style={{ width: 400, height: 300 }} alt="Cat" /> )} </div> </> ); } export default App;
이 코드에 대한 결과는 다음과 같습니다.
notion imagenotion image
 

1) Recoil의 Selector와 React의 Suspense를 통한 비동기 데이터 처리

앞의 장에서 설명한 바와 같이 Selector는 리코일의 상태 값을 변환하고 계산하는 함수입니다. 이를 통해 애플리케이션의 다양한 부분에서 필요한 데이터를 효율적으로 추출하고 공유할 수 있습니다. Suspense는 리액트의 비동기 데이터 처리(예: 데이터 가져오기, 코드 분할)를 위한 메커니즘 중 하나로 비동기 작업이 완료 되기를 기다리는 동안 UI를 정지시키지 않고 로딩 상태를 관리하여 더 직관적인 작업이 가능합니다. 리액트 프로젝트에서는 상태 관리와 비동기 작업을 더 효율적으로 처리하기 위해서 Selector와 Suspense를 함께 사용할 수 있습니다.
 
먼저 리코일 셀렉터를 catImageState라는 이름으로 정의합니다. 이 셀렉터는 고유한 키(key)를 가지며, 비동기로 데이터를 가져오는 get함수가 정의되어 있습니다. 이 함수는 API를 호출하고 고양이 이미지 데이터를 반환합니다.
// selector/catImageState.js import { selector } from "recoil"; // Recoil Selector를 정의합니다. export const catImageState = selector({ key: "catImageState", // 고유한 키로 식별합니다. get: async ({ get }) => { const url = "<https://api.thecatapi.com/v1/images/search?limit=1>"; const api_key = "DEMO_API_KEY"; try { // 고양이 이미지 데이터를 비동기로 가져옵니다. const response = await fetch(url, { headers: { "x-api-key": api_key, }, }); const data = await response.json(); return data[0]; } catch (error) { throw error; // 에러 발생 시 예외 처리 } }, });
 
catImageState 셀렉터를 사용하여 고양이 이미지 데이터를 가져와 화면에 출력하기 위해 useRecoilValue훅을 사용하여 셀렉터에서 값을 가져옵니다. 그리고 고양이 이미지를 imageTag로 렌더링 합니다.
import React, { Suspense } from "react"; import { useRecoilValue } from "recoil"; import { catImageState } from "./selector/catImageState"; function CatImage() { // useRecoilValue로 Selector에서 비동기 데이터를 가져옵니다. const imageData = useRecoilValue(catImageState); return ( <img src={imageData.url} style={{ width: 400, height: 300 }} alt="고양이" /> ); } function App() { return ( <> <h1>귀여운 고양이 이미지</h1> <div> <Suspense fallback={<p>로딩 중...</p>}> <CatImage /> {/* CatImage 컴포넌트를 Suspense로 감싸 로딩 중일 때 대체 내용을 표시합니다. */} </Suspense> </div> </> ); } export default App;
위와 같이 렌더링 할 때 Suspense를 통해 데이터가 로딩 중인 동안 표시할 대체 UI를 지정할 수 있습니다. 이 기능을 사용하면 데이터가 사용 가능할 때까지 fallback 값으로 부여된 로딩 표시기나 로딩 메시지와 같은 대기 상태를 나타내는 UI를 대신 렌더링이 가능합니다. 비동기 데이터가 로드 완료되면 fallback 대신에 실제 데이터가 포함된 UI가 표시됩니다. 데이터 로드 중 오류가 발생한 경우에도 Suspense의 fallback을 사용하여 사용자에게 오류 메시지나 오류 처리를 표시할 수 있습니다.
 
이제 index.js에서 리코일을 사용하기 위해 RecoilRoot로 App 컴포넌트를 감싸고 결과 화면을 확인해보겠습니다. (이후로는 이 과정을 생략하겠습니다.)
import React from "react"; import ReactDOM from "react-dom/client"; import { RecoilRoot } from "recoil"; // Import RecoilRoot import App from "./App"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <RecoilRoot> <App /> </RecoilRoot> );
 
notion imagenotion image
데이터를 가져오는 동안 로딩하는 페이지가 지속되다가 데이터 로딩이 완료되면 ‘로딩 중…’ 문구가 고양이 이미지로 대체됩니다.

2) Recoil의 Selector와 useRecoilValueLoadable을 통한 비동기 데이터 처리

useRecoilValueLoadable는 리코일 라이브러리에서 제공하는 훅 중 하나로, 비동기 리코일 상태에 접근하고 그 상태에 대한 로딩 상태를 처리하는 데 사용됩니다. 이 훅은 상태가 로딩 중인지, 에러가 발생했는지, 아니면 데이터가 성공적으로 로드 되었는지 확인할 수 있는 기능을 제공합니다. 상태에 대한 종류는 다음과 같습니다.
  • hasValue
이 상태는 비동기 리코일 상태가 데이터를 성공적으로 로드된 상태를 나타냅니다. 이 경우, contents 속성을 통해 로드된 데이터에 접근할 수 있습니다.
  • loading
이 상태는 비동기 리코일 상태가 아직 로딩 중임을 나타냅니다. 데이터가 아직 로드되지 않았으며, 로딩 중 메시지를 표시하거나 대기할 수 있습니다.
  • hasError
이 상태는 비동기 리코일 상태의 데이터 로딩 중에 오류가 발생한 상태를 나타냅니다. 이 경우, 데이터 로딩 중에 오류 메시지를 표시하거나 에러 처리를 수행할 수 있습니다.
이 상태 반환 값을 통해 비동기 리코일 상태의 로딩 상태를 확인하고 이에 따라 다른 처리의 수행이 가능합니다. useRecoilValueLoadable을 Selector와 함께 사용하면 비동기 데이터를 리코일 상태로 관리하고 이 데이터를 효과적으로 활용할 수 있습니다.
 
셀렉터에 대한 코드는 위에서 정의한 catImageState를 재사용하기 때문에 생략하겠습니다. 셀렉터에서 고양이 이미지 데이터를 비동기로 가져오고, useRecoilValueLoadable 훅의 인자로 받아 사용하여 로딩 상태를 처리합니다. 이에 따라 각각 다른 처리를 정의해주고, App 컴포넌트에서 이 처리에 따라 다른 렌더링을 해줍니다.
import React, { Suspense } from "react"; import { useRecoilValueLoadable } from "recoil"; import { catImageState } from "./selector/catImageState"; function CatImage() { // useRecoilValueLoadable을 사용하여 catImageState Recoil 상태의 로딩 상태를 확인합니다. const catImageLoadable = useRecoilValueLoadable(catImageState); if (catImageLoadable.state === "hasValue") { // 상태가 'hasValue'인 경우, 데이터가 성공적으로 로드된 것입니다. const imageData = catImageLoadable.contents; // 데이터를 추출합니다. return ( <img src={imageData.url} style={{ width: 400, height: 300 }} alt="고양이" /> ); } else if (catImageLoadable.state === "loading") { // 상태가 'loading'인 경우, 데이터가 아직 로드 중입니다. return <p>로딩 중...</p>; } else { // 그 외의 경우, 데이터 로딩 중에 오류가 발생했습니다. return <p>고양이 이미지를 불러오는 중 에러가 발생했습니다.</p>; } } function App() { return ( <> <h1>귀여운 고양이 이미지</h1> <div> <Suspense fallback={<p>로딩 중...</p>}> {/* Suspense 컴포넌트를 사용하여 CatImage 컴포넌트를 렌더링하고, 데이터 로딩 중에 로딩 상태를 처리합니다. */} <CatImage /> </Suspense> </div> </> ); } export default App;
결과 화면을 확인해보겠습니다.
notion imagenotion image
 
이번에도 로딩 중이라는 문구가 뜨다가 데이터 로딩이 완료되면 고양이 이미지로 대체되는 것을 확인할 수 있습니다. useRecoilValueLoadable의 반환 값인 catImageLoadable과 그의 프로퍼티인 state값을 콘솔에 찍어보면 다음과 같이 로딩 상태에 따라 달라지는 것을 확인할 수 있습니다.
notion imagenotion image
 

3) Recoil의 Atom Effects를 사용한 비동기 데이터 처리

비동기 Selector 대신, Atom Effects를 사용하여 비동기 작업을 처리할 수 있습니다. Atom Effects는 리코일의 최신 버전에서 비동기 작업을 관리하는 데 사용할 수 있는 기능이며, 이를 통해 아톰 상태의 변경에 반응하여 비동기적인 작업을 수행하고 그 결과를 상태에 저장할 수 있습니다.
 
atom effects는 기본적으로 아톰을 생성할 때 설정한 effects 프로퍼티에 할당된 함수들을 실행하는 방식으로 동작합니다. atom을 정의할 때 effects 프로퍼티를 사용하면 아톰이 초기화 될 때 실행되어야 하는 함수나 이펙트들을 정의할 수 있는데, 이 함수들은 아톰의 값이 읽히거나 설정될 때 실행됩니다. 이를 사용하여 아톰의 상태 변화에 반응하거나 비동기 작업을 처리하는 로직을 구현할 수 있게 해줍니다.
 
아래는 Atom Effects를 사용하여 비동기 작업을 처리하는 예제 코드입니다. atom.js에 catImageState이라는 이름의 atom을 만들고, key default값을 설정합니다.
//atom.js export const catImageState = atom({ key: 'catImageState', default: null, });
 
그리고 비동기 데이터를 가져오기 위한 함수를 정의합니다.
// 비동기 데이터를 가져오는 함수 const fetchCatImageAsync = async () => { const url = '<https://api.thecatapi.com/v1/images/search?limit=1>'; const api_key = 'DEMO_API_KEY'; try { const response = await fetch(url, { headers: { 'x-api-key': api_key, }, }); const data = await response.json(); const firstImageData = data[0]; return firstImageData.url; } catch (error) { throw error; } };
fetchCatImageAsync 함수는 외부 Api에서 고양이 이미지 Url을 가져오는 비동기 작업을 수행합니다.
 
Atom 및 Atom effects 를 정의합니다.
// Atom과 Atom Effect 정의 import { atom } from 'recoil'; export const catImageState = atom({ key: 'catImageState', default: null, effects_UNSTABLE: [ ({ onSet }) => { // Atom이 읽힐 때(fetch) 비동기 작업 수행 fetchCatImageAsync().then(url => { // 비동기 작업 완료 후 Atom 상태 변경 onSet(url); }); }, ], });
catImageState 아톰은 비동기적으로 데이터를 가져오고 읽힐 때마다 atom effect가 실행됩니다. atom effect 내에서는 fetchCatImageAsync 함수가 호출되어 데이터를 가져오고, 그 결과가 아톰의 상태로 설정됩니다. 이렇게 함으로써 아톰이 설정될 때마다 비동기 작업이 수행되고 작업이 완료되면 onset() 함수를 사용하여 아톰의 값을 업데이트합니다.
 
이후 app.js에서 useRecoilValue() hook의 인자를 catImageState로 변경하여 구독하도록 하면, Selector를 이용한 방법과 동일한 결과를 얻을 수 있습니다.
notion imagenotion image

4.3.3 비동기 Selector와 Atom Effects의 차이점

Selector와 Atom Effects 중 어떠한 방법을 사용해도 비동기 데이터를 가져올 수 있습니다. Selectors는 주로 동기적으로 동작하며, 리덕스 상태에서 데이터를 추출하거나 계산하는 데 사용됩니다. 이는 상태의 특정 부분을 선택하고 변환하는 역할을 합니다. 즉, Selectors는 현재 상태에서 값을 동기적으로 가져와 그 값을 기반으로 계산할 수 있습니다.
 
Atom effects는 주로 외부 API 호출, 데이터베이스 요청, 파일 I/O와 같은 비동기 작업을 다루는 데 사용됩니다. Atom effects는 리덕스 미들웨어를 사용하여 비동기적인 작업을 수행하고, 작업이 완료되면 새로운 액션을 발생시켜 상태를 업데이트합니다. 이렇게 함으로써 애플리케이션은 비동기 작업을 효과적으로 수행하고 상태를 업데이트할 수 있게 됩니다.
 
즉, Selectors는 동기적이고 주로 상태에서 값을 추출하거나 계산하는 데 사용되며, Atom effects는 비동기적이고 주로 외부 리소스와의 상호 작용이나 비동기 부작용을 처리하는 데 사용됩니다.

4.3.4 마무리

이상으로 리코일의 사용법에 대해 학습하고 예제까지 실습해 보는 시간을 가졌습니다. 기존의 상태 라이브러리와 비교했을 때 현저히 적은 코드 양과 수많은 보일러 플레이트 코드를 설정할 필요가 없다는 걸 볼 수 있었습니다.
또한 앞에서 살펴봤던 비동기 데이터, 상태 지속성, 매개변수화된 Selector를 처리할 수 있는 솔루션을 제공하기 때문에 러닝커브도 다른 라이브러리보다 낮습니다. 하지만 모든 상태를 전역 상태로 만들 필요가 없다면,  전역 변수의 사용을 최소화하고 , 필요한 상태만 전역으로 관리해야 성능과 유지 보수 측면에서 이점을 누릴 수 있습니다.
따라서 여러 상태들을 각 용도에 맞게 잘 구분하고, 언제 어떤 주기로 갱신이 필요한지도 예측하고 사용한다면 보다 사용성 높은 어플리케이션을 만들 수 있을 것입니다.