https://snowman-seolmini.tistory.com/84
[React Project] 파지직TV⚡[Ver2.0]
Intro✍️기존의 사이트를 자신만의 것으로 만들어 보는 것은 리액트를 공부하고 연습해 보는 의미에서 큰 의미가 있다고 생각한다. 파지직TV사이트는 학습한 리액트 지식을 활용하고, 더 나은
snowman-seolmini.tistory.com
파지직TV에서 수많은 비디오 호출 메서드들은 싱글톤 패턴으로 이뤄져 있다. 이게 무슨 소리냐, 하나의 클래스 안에 많은 호출 메서드들을 두고 이를 Context AP를 사용해 모든 컴포넌트에서 하나의 인스턴스로 여러 메서드들을 사용하는 것이다.
이러한 방식을 이용하면 리소스를 줄일 수 있다고 믿었고, 이를 직접 확인하고자, 인스턴스의 생성을 콘솔로그로 살피고, 힙 스냅샷을 이용해 싱글톤 적용 전/후의 메모리를 체크해 봤다.
힙 스냅샷
- 힙 메모리(Heap Memory)는 JS가 객체와 데이터를 저장하는 메모리 영역이다.
- JS가 객체, 배열, 함수 등 여러 데이터를 위해 할당한 메모리의 크기를 의미한다. 이를 측정하는 테스트가 힙 스냅샷이다.
* 역할 : 특정 시점의 힙 메모리 상태를 캡처하여 모든 객체, 함수, 배열, DOM 요소가 차지하는 메모리 양을 보여줌
*사용 용도 : 앱에서 특정 시점의 전체 메모리 사용 상태를 분석하고 싶을 때 유용하다. 특히 메모리 누수(메모리를 반환하지 않는 객체가 계속 남아있는 현상)를 찾을 때 많이 사용된다.
힙 스냅샷의 위치를 어떻게 설정하였는가?
1. 페이지 로딩 직후:
- 페이지가 초기화 때
- 모든 초기화 작업이 이루어지고, 필요한 데이터가 로딩되기 시작되기 때문에.
2. 상호작용 직후 (버튼 클릭, 데이터 로딩 등):
- 페이지 내에서 사용자 상호작용이 이루어질 때
- 사용자 인터랙션이 발생하고 나서, 데이터를 추가로 로딩할 때 메모리 사용량이 어떻게 변하는지 확인
3. 데이터 업데이트 직후:
- 데이터 업데이트(새로운 비디오 리스트가 로드되는 경우) 후에 메모리 사용량을 체크
- 데이터가 동적으로 업데이트될 때, 객체가 추가되거나 캐시가 늘어날 수 있기 때문에
4. 컴포넌트가 렌더링되거나 사라질 때:
- React에서는 컴포넌트가 렌더링될 때 또는 사라질 때 메모리 사용량이 늘어날 수 있기 때문에
- 예를 들어, 사용자가 카테고리를 전환할 때마다 새로운 비디오 목록을 로드하고, 이때마다 새로운 컴포넌트가 렌더링되기 때문에 메모리 사용량이 변할 수 있기 때문에
- 컴포넌트가 생성될 때와 컴포넌트가 사라질 때의 메모리 차이를 확인하면서 메모리 누수가 있는지 체크
어..?
싱글톤을 적용하기 전/후가 별 차이 없거나, 오히려 적용 전이 메모리가 적게 나온다...(이럴수가 너..만능이 아니구나...)
여기서 몇 가지 이유를 생각 해봤다.
1. 싱글톤 자체는 메모리(리소) 사용량을 줄이지 않는다.
- 싱글톤 패턴은 인스턴스를 하나로 제한하여 여러 번 생성되는 것을 방지하지만, 객체를 생성하는 과정에서의 메모리 사용량은 싱글톤이든 아니든 동일하다.
- 따라서, 인스턴스 수가 적거나 애플리케이션 구조 상 이미 필요 이상으로 객체를 생성하지 않고 있었다면, 싱글톤 적용으로 메모리 절감 효과는 거의 없다.
2. 싱글톤으로 인해 참조가 오래 지속될 수 있다.
- 일반적으로 여러 인스턴스를 생성하면, 사용되지 않는 인스턴스들이 빠르게 GC에 의해 정리된다. 하지만 싱글톤으로 인스턴스를 전역으로 유지하면 GC가 이를 정리할 수 없기 때문에, 메모리에 계속해서 남아있게 된다. 반면 여러 컴포넌트가 각자 인스턴스를 생성하면 GC가 사용하지 않는 인스턴스를 더 자주 제거해 줄 수 있어 일시적으로 메모리 사용량이 줄어들 수 있다.
- 싱글톤 인스턴스는 애플리케이션 전체 라이프사이클(TTL) 동안 유지된다. 이는 Garbage Collector(GC)가 메모리를 회수하지 못하게 만드는 경우
- 예를 들어, 메서드 호출 결과를 계속 메모리에 저장하고 있다면, 이런 데이터가 불필요하게 오래 유지될 가능성이 있다.
3. Context API의 사용 방식
- Context API를 사용해서 전역 상태로 인스턴스를 관리하는 경우, React에서는 해당 Context가 업데이트될 때마다 이를 구독하고 있는 모든 컴포넌트가 다시 렌더링된다. 이런 재렌더링 과정에서 Context API의 사용이 오히려 메모리 사용량을 증가시키는 원인이 될 수 있다.
싱글톤이 리소스를 줄여주지 않는다면...
오히려 GC가 늦게 처리된다면...
누적되어서 메모리가 더 증가했다면...
다른 경로를 최적화하는 게 낫다면....
- 1. API 호출 클래스에 캐시 매커니즘 추가
- 2. 데이터 패칭 최적화
- 3. 병렬 데이터 패칭
- 4. Code Splitting (Lazy Loading 및 Supsense 활용)
이렇게 4가지 방법을 생각해봤다.
1. API 호출 클래스에 캐시 매커니즘 추가
- 일정 시간 동안 API 호출이 일어나면 결과값을 저장 및 재사용을 통해 불필요한 호출을 줄이는 방법이다.
- 캐시된 데이터가 있을 때 API 호출을 생략하므로 응답 속도가 빨라진다.
- 클래스 내부에 캐시 메커니즘을 캡슐화하여 외부에서는 이를 신경 쓰지 않고 사용가능하므로 코드 유지보수에도 문제 없다.
이미 이 역할을 React Query가 훌륭하게 하고 있다. - 패스!
2. 데이터 패칭 최적화
- React Query를 사용하고 있다면 캐싱 관련 주요 옵션들을 잘 적용하고 있는가를 체크해봐야 한다.
#1. staleTime (데이터가 오래된 것으로 간주되기까지의 시간)
#2. gcTime (캐시에 남아있는 시간)
속성 | staleTime | cacheTime |
역할 | 데이터가 "최신 상태"로 간주되는 시간 | 데이터가 메모리에 유지되는 시간 |
영향 범위 | 컴포넌트가 데이터를 요청하는 시점 | 데이터가 메모리에서 삭제되는 시점 |
기본값 | 0 (항상 오래된 데이터) | 5분 |
관련 동작 | staleTime 내에서는 캐싱된 데이터만 반환 | gcTime 이후에는 캐시 데이터 삭제 및 새 호출 |
활용 예시 | 실시간성 데이터를 자주 새로고침해야할 때 | 오래된 데이터를 자동으로 정리해 메모리를 관리 |
실제 동작 흐름
예 : staleTime (5분), gcTime (10분)
1. 컴포넌트가 데이터를 처음 요청
- API 호출 후 데이터가 캐싱되고, "최신 상태"로 간주 (staleTime : 5분)
2. 5분 내에 다시 컴포넌트를 방문
- 데이터는 여전히 "최신 상태"로 간주.
- 백그라운드 요청 없이 캐싱된 데이터 반환.
3. 5분 이후, 10분 내에 방문
- 데이터는 "오래된 상태"로 간주.
- 백그라운드에서 데이터 재요청.
4. 10분 이후 방문
- 캐싱된 데이터가 메모리에서 삭제.
- 새로운 API 호출 발생
백그라운드에서 재요청하는 것과 API를 재호출하는 것의 차이는 사용자가 데이터를 볼 때 업데이트가 보이는 방식과 관련이 있다.
- 백그라운드에서 재요청 : 사용자가 이미 데이터를 보고 있는 상태에서, React Query가 몰래 새 데이터를 가져온다. 기존 데이터를 유지하면서 업데이트된 데이터를 백그라운드에서 추가로 가져온다. 사용자는 데이터가 깜박이지 않고 자연스럽게 최신 상태로 갱신된다.
- API 재호출 : 데이터를 가져오는 동안 기존 데이터를 지우고 로딩 상태로 돌아간다. 새 데이터를 가져오기 전까지 사용자는 아무것도 보지 못하거나 "Loading" 화면만 보게 된다.
#3. refetchOnWindowFocus (창이 다시 초점을 맞출 때 쿼리가 자동으로 데이터를 다시 가져오는지 여부)
#4. placeholderData: keepPreviousData (새 데이터를 가져오는 동안 이전 데이터를 유지)
위의 설명을 바탕으로 PazizicTV 호출 스타일을 설정하면
예시
useQuery('videos', fetchVideos, {
staleTime: 5 * 60 * 1000, // 데이터는 5분 동안 최신 상태로 간주
cacheTime: 10 * 60 * 1000, // 캐시 데이터는 10분 동안 유지
refetchOnWindowFocus: false, // 브라우저 포커스 시 재요청 비활성화
placeholderData:keepPreviousData, // 기존 데이터 유지
});
3. 병렬 데이터 패칭
현재 각 컴포넌트에 한 개의 데이터를 패칭하는 상황이 많다. 컴포넌트의 독립성을 높일려고, 컴포넌트마다 1개의 기능을 담당하도록 했기 때문이다.
만약 한 부모 컴포넌트 안에 자식 컴포넌트 2개가 각각 1개의 데이터 패칭을 하고 있다면, 병렬로 데이터를 패칭하도록 통합하도록 하여 렌더링 시간 및 리소스를 줄일 수 있다.
이를 위해 일부로 분리해둔 자식 컴포넌트를 반드시 합칠 필요는 없다. 서로 복잡하고 긴 코드를 가지고 있다면, 부모 컴포넌트 위치에서 데이터 패칭을 해 데이터를 props로 보내면 그만이다.
4. Code Splitting 적용
예를 들어, 비디오 영상에 따른 댓글, 프로필 같은 컴포넌트가 초기 렌더링에 반드시 필요한 것이 아니라면 lazy와 Suspense를 활용해 지연 로딩을 적용할 수 있다.
예시
import React, { lazy, Suspense } from 'react';
const ChannelInfo = lazy(() => import('./ChannelInfo'));
const RelatedVideos = lazy(() => import('./RelatedVideos'));
const VideoDetail = ({ channelId, videoId }) => {
return (
<Suspense fallback={<p>Loading Components...</p>}>
<ChannelInfo channelId={channelId} />
<RelatedVideos videoId={videoId} />
</Suspense>
);
};
- 초기 번들 크기를 줄여 로딩 성능 개선.
- 실제 필요한 시점에만 컴포넌트를 로드.
- 코드가 비동기적으로 로드되므로, 렌더링 중 깜박임 현상이 발생할 수 있음(fallback을 사용자 경험에 맞게 디자인 필수)
싱글톤은 객체 인스턴스의 생성 비용을 줄이고, 상태를 공유하는 데 유리한 디자인 패턴이지만, 리소스 사용량을 직접 줄이는 것과 다르다는 것을 깨달았다.
* API 호출 자체는 싱글톤으로 줄어들지 않는다.
- 싱글톤 패턴은 객체를 한 번만 생성하고, 모든 호출이 동일한 인스턴스를 사용하도록 보장하지만, API 호출 빈도는 싱글톤 여부와 직접적인 관련이 없다.
- 싱글톤이 하는 일은 호출 메서드가 여러 번 호출되더라도 동일한 인스턴스를 사용하므로 메모리에서 중복된 객체를 생성하지 않음.
- 동일한 데이터를 요청할 때마다 API를 다시 호출하는 것을 방지하지는 않음.
즉, 싱글톤 패턴으로 객체 생성 비용은 줄일 수 있지만, 동일한 데이터를 캐싱하거나 호출 빈도를 줄이는 것은 별도의 로직이 필요하다. 예를 들어, 비동기 관련 라이브러리를 사용하거나, 캐싱 메커니즘을 추가해야 한다.
* 메모리 관리 문제
- 싱글톤이 데이터를 보유하거나 캐싱하는 방식이 효율적이지 않은 경우, 오히려 메모리를 낭비할 수 있다.
- 캐싱된 데이터가 오래 유지되면, 불필요한 데이터가 메모리를 차지하게 될 수 있음.
결론
싱글톤은 별도의 캐시 관리와 최적화 로직이 함께 있을 때 빛을 낸다...
'Project' 카테고리의 다른 글
[React Project] Momentum - Lighthouse 최적화 (5) | 2024.11.06 |
---|---|
[React Project] Momentum ver 1 .1 .1 리팩토링 과정 중 문제&해결 (0) | 2024.10.18 |
[React Project] Momentum ver 1 .0 .1 (0) | 2024.10.08 |
[React Project]F.ROCK🥻(Shopping mall) (0) | 2024.08.15 |
[React Project] 파지직TV⚡[Ver2.0] (0) | 2024.07.27 |