Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ Feat ] 컴포넌트 별 코드 스플릿팅 #210

Merged
merged 2 commits into from
Oct 23, 2024

Conversation

KIMGEONHWI
Copy link
Member

@KIMGEONHWI KIMGEONHWI commented Oct 18, 2024

🔥 Related Issues

✅ 작업 리스트

  • Calendar 컴포넌트 lazy로딩 적용
  • Suspense 적용
  • mouseover 속성 활용하여 preload 적용

🔧 작업 내용

ReactSPA(Single-Page-Application)인데, 사용하지 않는 모든 컴포넌트까지 초기에 한 번에 불러오기 때문에 첫 화면이 렌더링 될때까지의 시간이 오래걸린다는 단점이 존재합니다. 저희 Morib프로젝트에서도 초기 HomePage 랜더링시 많은 용량을 차지하고 있는 DatePicker 번들을 불러오고 있었습니다. 그래서 Calendar 컴포넌트를 lazy로딩suspense로 동적으로 컴포넌트를 임포트를하게하여 DatePicker가 필요한 시점에 불러오게하여 사용하는 방법으로 성능 최적화를 진행하였습니다.

CalendarBoxCategory, ModalAddCategory 컴포넌트에 사용되므로 2개의 컴포넌트에서 Calendar 컴포넌트에 lazy를 적용해주었습니다.

const Calendar = lazy(() => import('@/shared/components/Calendar'));

다음과 같이 Calendar에 lazy를 걸어주고

<Suspense fallback={<div>Loading...</div>}>
    <div className="absolute left-[7.25rem] top-[9.5rem]">
	<Calendar
		isPeriodOn={isPeriodOn}
		selectedStartDate={selectedStartDate ?? defaultDate}
		selectedEndDate={selectedEndDate ?? null}
		onStartDateInput={handleStartDateInput}
		onEndDateInput={handleEndDateInput}
		isCalendarOpened={isCalendarOpened}
		onPeriodToggle={handlePeriodToggle}
		clickOutSideCallback={handleCreatePost}
	/>
    </div>
</Suspense>

Suspense도 적용해 주었습니다. lazy를 통해 import하면 해당 컴포넌트를 불러오는 시점에 로딩하는 시간이 생기게 됩니다. Suspense는 아직 렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩 화면을 보여주고, 로딩이 완료되면 렌더링이 준비된 컴포넌트를 보여주는 기능입니다.

하지만 lazy로딩과 suspense로 동적으로 컴포넌트를 임포트를하게 되면 해당 컴포넌트를 사용하는 시점에 로딩이 발생할 수 있어서 오히려 성능을 저해하거나 사용자 경험을 저해하지 않을까?🤔

그래서 Preloading을 활용하여 해당 문제를 해결하고자 하였습니다. onMouseEnter의 속성을 활용하여 lazy로딩이 적용된 컴포넌트가 사용되는 시점을 미리 예측하여 불러오게 하였습니다.
BoxCategory컴포넌트에서는 + 버튼에 mouseover시, ModalAddCategory 컴포넌트에서는 날짜 선택 토글에 mouseoverlazy로딩이 적용된 Calendar컴포넌트를 불러오도록 하였습니다.

usePreloadCalendar 커스텀 훅 생성

import { useCallback, useState } from 'react';

export const usePreloadCalendar = () => {
	const [isCalendarLoaded, setIsCalendarLoaded] = useState(false);

	const preloadCalendarComponent = useCallback(() => {
		if (!isCalendarLoaded) {
			import('@/shared/components/Calendar').then(() => {
				setIsCalendarLoaded(true);
			});
		}
	}, [isCalendarLoaded]);

	return { isCalendarLoaded, preloadCalendarComponent };
};

BoxCategory, ModalAddCategory 컴포넌트에서 공통적으로 Calendar를 preload하고 있기 때문에, Calendar를 preload하는 로직을 커스텀 훅으로 분리하여 재사용 가능하도록 하였습니다.

🧐 새로 알게된 점

  • onMouseEnter 속성
  • lazy로딩과 Suspense의 관계
  • preload를 하는 이유

🤔 궁금한 점

처음 해보는 코드 스플리팅 작업이어서 제대로 했는지 감이 안잡힙니다. 많은 피드백과 의견 부탁드려요!

📸 스크린샷 / GIF / Link

화면 기록 2024-10-17 오후 8 53 36
onMouseEnter 속성을 활용하여, 날짜 토글 위에 마우스가 올라오면 Calendarpreload 할 수 있게 하였습니다. 날짜 토글 위에 마우스가 올라와야 네트워크탭에 Calendar를 불러오고 있는 것을 확인할 수 있다.

자세한 내용과 성능 지표 향상 정도는 아티클 참고부탁드립니다.
https://velog.io/@geonhwi1014/React-%EC%BD%94%EB%93%9C-%EC%8A%A4%ED%94%8C%EB%A6%AC%ED%8C%85code-spliting-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94

@KIMGEONHWI KIMGEONHWI added the ✨ Feature 기능 개발 label Oct 18, 2024
@KIMGEONHWI KIMGEONHWI self-assigned this Oct 18, 2024
@KIMGEONHWI KIMGEONHWI linked an issue Oct 18, 2024 that may be closed by this pull request
1 task
Copy link
Member

@suwonthugger suwonthugger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 고생하셨어요. 번들 구조, 네트워크 탭, 라이트하우스 지표를 보며 필요한 번들을 불러오는 과정에 대해서 정리해주셔서 이해가 쉬웠어요. 리뷰에 대해서만 확인 부탁드릴게요.

Comment on lines 1 to 15
import { useCallback, useState } from 'react';

export const usePreloadCalendar = () => {
const [isCalendarLoaded, setIsCalendarLoaded] = useState(false);

const preloadCalendarComponent = useCallback(() => {
if (!isCalendarLoaded) {
import('@/shared/components/Calendar').then(() => {
setIsCalendarLoaded(true);
});
}
}, [isCalendarLoaded]);

return { isCalendarLoaded, preloadCalendarComponent };
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1:

  • 캘린더를 preloading 할 때 useState, useCallback 없이 잘 작동하는것으로 확인되는데 사용하신 이유가 궁금해요!
  • useState, useCallback 사용 없이 preloading 하는것을 제안드리고 싶어요, 그렇게 되면 커스텀훅을 만들필요도 없을것 같은데 어떻게 생각하나요?!
const handleMouseEnter = () => {
  import('@/shared/components/Calendar');
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1:

  • 캘린더를 preloading 할 때 useState, useCallback 없이 잘 작동하는것으로 확인되는데 사용하신 이유가 궁금해요!
  • useState, useCallback 사용 없이 preloading 하는것을 제안드리고 싶어요, 그렇게 되면 커스텀훅을 만들필요도 없을것 같은데 어떻게 생각하나요?!
const handleMouseEnter = () => {
  import('@/shared/components/Calendar');
};

useStateuseCallback을 사용하지 않는다면, 캘린더가 이미 로드되었는지 성공적으로 로드되었는지 추적이 불가능하기 때문에 중복된 import 요청이 발생하지 않을까?라는 우려가 있었습니다. 하지만, 대원님의 의견대로 진행하여도 동일한 모듈을 여러 번 import하는 경우가 발생해도 이미 로드된 모듈이면 즉시 캐시된 결과를 반환하여 중복 요청을 막을 수 있다고 하네요! 실제로 확인해본 결과 네트워크창에서 한번 로드되었으면 재로드를 하지 않는것을 확인하였습니다. 의견 반영하겠습니다.

Copy link
Member

@suwonthugger suwonthugger Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자세한 설명 감사드려요~~

  • 말씀해주신대로 동적 import할 때는 브라우저 수준에서 캐시가 되므로, 중복된 네트워크 요청으로 번들을 로드 하는 사항은 고려하지 않아도 괜찮을것 같아요~
  • 성공적인 번들 로드에 대해서 언급해주셔서 생각하보니 추가적으로 캘린더 컴포넌트를 동적으로 import 하는 동작을 실패할 경우에 대해서 고려해야할것 같네요. 네트워크가 갑자기 끊기거나 하면 실패할수도 있겠네요. 다음과 같이 에러가 발생했는지 파악할 수 있게 간단하게 console error 코드를 추가하면 어떨까해요!

충분한 예외 사항 고려감사합니다. 하나 배워갑니다~

handleMouseEnter = () => {
  import('./components/ImageModal')
    .catch((error) => {
      console.error('캘린더를 받아오는데 오류가 발생했습니다.', error);
    });
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자세한 설명 감사드려요~~

  • 말씀해주신대로 동적 import할 때는 브라우저 수준에서 캐시가 되므로, 중복된 네트워크 요청으로 번들을 로드 하는 사항은 고려하지 않아도 괜찮을것 같아요~
  • 성공적인 번들 로드에 대해서 언급해주셔서 생각하보니 추가적으로 캘린더 컴포넌트를 동적으로 import 하는 동작을 실패할 경우에 대해서 고려해야할것 같네요. 네트워크가 갑자기 끊기거나 하면 실패할수도 있겠네요. 다음과 같이 에러가 발생했는지 파악할 수 있게 간단하게 console error 코드를 추가하면 어떨까해요!

충분한 예외 사항 고려감사합니다. 하나 배워갑니다~

handleMouseEnter = () => {
  import('./components/ImageModal')
    .catch((error) => {
      console.error('캘린더를 받아오는데 오류가 발생했습니다.', error);
    });
};

에러코드 추가해주면 좋겠네요! 의견 반영하겠습니다.

Copy link
Member

@suwonthugger suwonthugger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생많으셨습니다. 레전드 개발자 김건휘 🚀🚀🚀🚀🚀

Copy link
Collaborator

@Ivoryeee Ivoryeee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

건휘님 고민 과정이 pr과 리뷰에 잘 담겨 있어 인상 깊게 보고 갑니다!
hook으로 분리하던 초기 방식에서 handleMouseEnter로 에러 처리까지 직관적으로 나타내는 방식으로 수정된 부분도 좋았습니다!
수고 많으셨어요 :) LGTM!!

@seueooo
Copy link
Collaborator

seueooo commented Oct 23, 2024

오.... onMouseEnter 속성으로 컴포넌트가 사용되는 시점을 미리 예측해서 preload하는거 너무 신기하고 좋은방법이네요...아티클도 꼼꼼히 작성해주셔서 이해하기 수월했습니다. 재밌게 읽고 갑니다!!Lgtm

@KIMGEONHWI KIMGEONHWI merged commit 8f56b73 into develop Oct 23, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

[ Feat ] 컴포넌트 별 코드 스플릿팅
4 participants