회사 프로젝트에 적용해볼 Atom 라이브러리 테스트 기록
현재 진행중인 프로젝트 특성상 폴링을 엄청나게 땡겨야 한다. 못해도 화면마다 api5개 이상을 5초~10초 단위로 땡겨야 함!
협력사에서도 우리쪽에서 보내는 이벤트를 interval로 땡겨야 하는데 atom이라는 라이브러리를 쓴다고 하시기에 나도 테스트해봄.
1. Library Atom from 'jotai'
- Jotai 는 React 애플리케이션에서 사용하는 경량 상태 관리 라이브러리.
- 여기서 말하는 atom 은 상태 단위를 의미.
- 쉽게 말하면, atom 하나가 전역적으로 관리되는 상태 하나
import { atom } from 'jotai';
// 1. atom 정의 (상태 생성)
const countAtom = atom(0);
// 2. atom 사용
import { useAtom } from 'jotai';
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
atom 을 쓰는 이유?
- 간단한 전역 상태 관리 (Redux, Recoil 보다 가볍고 빠름)
- 비동기 API 통합이 쉬움 (atom 안에 fetch, WebSocket 등 넣기 쉬움)
- 의존성 기반 업데이트 (atom 이 바뀌면 구독하는 컴포넌트만 다시 렌더링)
- 한 번만 API 관리 (PollingManager 하나로 끝)
- 여러 화면에서 같은 데이터 사용 가능
- 새로 고침 / 실시간 업데이트 신경 안 써도 됨
- 컴포넌트 간 데이터 전달할 필요 없음 (props drilling X)
- 신규 화면 추가해도 atom 구독만 하면 끝!
PollingManager (API 호출)
│
↓
[datacenterInfoAtom] ← 저장소
│ │
↓ ↓
panel1 panel2
2. Polling
리액트 특성상 뱅뱅 돌면서 좀 헷갈리는 부분이 있긴 한데
처음엔 [ PollingManager.tsx ] - [ UI컴포넌트.tsx ] 로 호출해서 쓰다가, URL이 워낙 많기때문에 3등분을 했다.
[ PollingManager.tsx ] - [ PollingConfig.ts ] - [ UI컴포넌트.tsx ]
PollingManager.tsx
- PollingConfig.ts에서 apiList를 배열로 넘기면 여기선 singlePolling 컴포넌트로 넘겨서 개별 fetch(interval)을 진행한다.
import {useEffect, useRef} from 'react';
import { useSetAtom, Atom } from 'jotai';
// API 항목 타입 정의
type ApiItem<T> = {
url: string;
atom: Atom<T>;
interval?: number;
};
// PollingManager props 타입
type PollingManagerProps = {
apiList: ApiItem<any>[]; // 제네릭으로 하려면 <T> 넘겨줘야 하고, 일단 any
interval?: number;
};
// PollingManager 컴포넌트
function PollingManager({ apiList, interval = 5000 }: PollingManagerProps) {
return (
<div>
{apiList.map(({ url, atom, interval: itemInterval }) => (
<SinglePolling
key={url}
url={url}
atom={atom}
interval={itemInterval ?? interval}
/>
))}
<div/>
);
}
// SinglePolling props 타입
type SinglePollingProps<T> = {
url: string;
atom: Atom<T>;
interval: number;
};
// SinglePolling 컴포넌트
function SinglePolling<T>({ url, atom, interval }: SinglePollingProps<T>) {
const setAtom = useSetAtom(atom);
const previousDataRef = useRef<T>();
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = (await response.json()) as { success: boolean; message: string; data: T };
// setAtom(result.data); 리랜더링을 막기위해 전후값을 비교하고 useRef로 처리함
if (JSON.stringify(result.data) !== JSON.stringify(previousDataRef.current)) {
setAtom(result.data);
previousDataRef.current = result.data;
}
} catch (error) {
console.error(`Polling error for ${url}:`, error);
}
};
fetchData(); // 초기 로딩
const timer = setInterval(fetchData, interval);
return () => clearInterval(timer); // 언마운트 시 정리
}, [url, atom, interval, setAtom]);
return null; // UI 없음 (배경에서 동작)
}
export default PollingManager;
PollingConfig.ts
- polling할 url들 리스트 생성.
- url, 구독할 atom이름, setInterval 설정
import {atom} from 'jotai';
type panel1Type = {
clctDate : string;
clctTime: string;
output: number;
}
type panel2Type = {
deviceId: string;
clctDate : string;
clctTime: string;
supplyTemperature: number;
}
export const panel1Atom = atom<panel1Type[] | null>(null);
export const panel2Atom = atom<panel2Type[] | null>(null);
type pollingApiItemType = {
url: string;
atom: any; // atom 타입 더 구체화 가능
interval: number;
};
// 필요한 api list 준비 (ui에서 호출하는 메서드)
export const getPollingApiList = (screenId: '1' | '2' | '3' | '4', deviceId: number): pollingApiItemType[] => {
switch (screenId) {
case '1': // 화면에 따라 url을 나눠서 처리 가능
return [];
case '2':
return [];
case '3':
return [];
case '4':
return [
{
url: `http://localhost:8080/panel1List?deviceId=${deviceId}`,
atom: panel1Atom, // 위 url은 이 atom이름으로 구독하면 됨
interval: 5000,
},
{
url: `http://localhost:8080/panel2List?deviceId=${deviceId}`,
atom: panel2Atom,
interval: 3000,
},
];
default:
return []; // 안전을 위해 default 도 명시
}
};
UIComponent.tsx
- UI에서 사용할때에는 pollingConfig.ts에 만들어둔 getPollingApiList에 인자 넘김 → 해당하는 apiList[] 반환받음 → PollingManager.tsx에 전달 → 결과값 useAtom(지정한 Atom이름) 으로 구독하여 받아옴 → 파싱하여 사용
const panel1 = memo(() => {
const [panel1] = useAtom(panel1Atom); // useAtom으로 해당 api구독 (저장된 값 읽어옴)
return <div>{panel1?.[0]?.clctTime ?? '데이터 없음'}</div>;
});
const panel2 = memo(() => {
const [panel2] = useAtom(panel2);
const clctTime = panel2?.[0]?.supplyTemperatrue ?? '데이터 없음';
return <div>{supplyTemperatrue}</div>;
});
// pollingManager를 바로 호출하지않고 useMemo로 감싸서 호출시 주변 리렌더링을 막음
const PollingWrapper = ({ deviceId }: { deviceId: number }) => {
const apiList = useMemo(() => getPollingApiList('4', deviceId), [deviceId]);
return <PollingManager apiList={apiList} interval={5000} />;
};
// [ 최종반환 컴포넌트 ]
const UIComponent = () => {
// atom test
const [deviceId, setDeviceId] = useState(1); // deviceId를 1로 세팅하여 PollingManager 호출
return(
<div>
<PollingWrapper deviceId={deviceId} /> // polling manager호출
<div>
<panel1 /> // ui렌더링
<panel2 />
</div>
<div/>
)
}
3. 예상되는 추후 개선사항
- 다른곳에서 폴링까지 받아서 값을 다 끼운 컴포넌트를 UIComponent에 import하는게 좀 더 깔끔. return문에서 pollingManager 호출도 하고 ui도 그리니까 조금 정신없음.
- 리액트 최고의 적 리렌더링...💥 아직 훅을 잘 못써서 디버깅해보면 같이 끌려와서 리렌더링되는 컴포넌트들이 있는데 조금더 최적화가 필요할것 같다.