- 앱을 실행시켰을 때 가장 처음으로 만나는 화면
- 앱의 첫인상을 결정하는 브랜드 아이덴티티를 표현할 수 있는 공간
- 런치 스크린(Launch, Launcher Screen)이라고도 합니다
- (시각적 즐거움보다) 사용자의 첫인상을 설계하고, 첫 방문 혹은 재방문 등 각각의 타입에 따라 다른 랜딩 화면을 제공하기 위한 준비 화면
- 유저들이 앱을 계속해서 이용할지 말지 결정하는 이유 중 하나가 되는 UI요소
- 사용자가 어떤 서비스인지 정확하게 모르는 상황에서, 전체 분위기를 가늠해 볼 수 있는 간판
- 앱 실행시마다 반복적으로 노출되므로 브랜딩의 일환으로 사용
- 짧게는 1초에서 5초 정도 노출. 보통 3초까지가 최대 (길어지면 사용자 이탈 가능성이 높음)
- 짧은 시간 내 앱의 이미지를 심어줍니다.
import { memo, useLayoutEffect, useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { SPLASH_TEXT } from '../../config';
interface SplashScreenProps {
fadeOutTime?: number;
}
const splashScreenElement = document.getElementById(
'splash-screen',
) as HTMLElement;
function SplashScreen({
fadeOutTime = 3000,
}: SplashScreenProps): JSX.Element | null {
const [currentText, setCurrentText] = useState('');
const [textIndex, setTextIndex] = useState(0);
useEffect(() => {
if (currentText.length < SPLASH_TEXT[textIndex].length) {
const timer = setTimeout(() => {
setCurrentText(
(prev) => prev + SPLASH_TEXT[textIndex][currentText.length],
);
}, 50);
return () => clearTimeout(timer);
} else {
const timer = setTimeout(() => {
if (textIndex < SPLASH_TEXT.length - 1) {
setTextIndex((prev) => prev + 1);
setCurrentText('');
}
}, 1000);
return () => clearTimeout(timer);
}
}, [currentText, textIndex]);
useLayoutEffect(() => {
if (!splashScreenElement) return;
const clearId = setTimeout(() => {
splashScreenElement.innerHTML = '';
}, fadeOutTime);
return () => {
clearTimeout(clearId);
};
}, [fadeOutTime]);
if (!splashScreenElement) return null;
return createPortal(
<div className='fixed inset-0 z-50 backdrop-blur-sm bg-foreground/50 max-w-screen-sm mx-auto w-full h-full bg-cover bg-center bg-no-repeat animate-fade-out opacity-100 delay-3000'>
<div className='absolute inset-0 flex flex-col items-center justify-center mx-auto gap-4'>
<Heart />
<div className='text-background text-xl min-h-[2rem]'>
{currentText}
<span className='animate-pulse'>|</span>
</div>
</div>
</div>,
splashScreenElement,
);
}
export default memo(SplashScreen);
function Heart() {
return (
<svg
version='1.1'
xmlns='http://www.w3.org/2000/svg'
className='w-[60px] h-[60px] fill-red-500 animate-heart-beat'
viewBox='-2 -2 28 28'
>
<path d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z' />
</svg>
);
}
-
createPortal
를 사용한 이유
-
createPortal
은 React의 createPortal
은 컴포넌트를 DOM 트리의 다른 위치에 렌더링할 수 있게 해줍니다.
- 스플래시 화면 같은 경우 다른 컴포넌트들과 다르게 독립적으로 존재해야합니다.
- 스플래시 화면은 최상위 레벨에 위치하여 전체 화면을 덮어야하기 때문입니다.
- index.html에
id="splash-screen"
인 별도의 DOM 요소에 렌더링함으로써 메인 애플리케이션 컴포넌트 트리와 분리됩니다.
<body>
<div id="splash-screen"></div>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
- 이렇게 하면 스플래시 화면의 스타일링과 애니메이션이 다른 컴포넌트에 영향을 주지 않습니다.
-
memo
를 사용한 이유
-
memo
는 컴포넌트의 불필요한 리렌더링을 방지합니다.
- 스플래시 화면은 부모 컴포넌트가 리렌더링될 때마다 다시 렌더링될 필요가 없습니다.
- 스플래시 화면의 props(fadeOutTime)가 변경되지 않는 한, 컴포넌트를 다시 렌더링할 필요가 없습니다.
- 성능 최적화를 위해 사용하였습니다.
-
useLayoutEffect
를 사용한 이유
-
useLayoutEffect
는 DOM 변경 후 브라우저가 화면을 그리기 전에 동기적으로 실행됩니다.
- 스플래시 화면을 제거하는 작업은 시각적인 깜빡임 없이 부드럽게 이루어져야 합니다.
- useEffect와 달리 useLayoutEffect는 브라우저가 화면을 그리기 전에 실행되므로, 스플래시 화면 제거 시 시각적 깜빡임을 방지합니다.
- splashScreenElement.innerHTML = ''를 통해 스플래시 화면을 DOM에서 직접 제거하는 방식을 사용했습니다.
-
useState
와 useEffect
를 활용한 타이핑 효과
- 타이핑 효과를 구현하여 텍스트가 한 글자씩 나타나는 애니메이션을 만듭니다.
- currentText 상태는 현재까지 표시된 텍스트를 저장합니다.
- textIndex 상태는 현재 표시 중인 텍스트의 인덱스를 추적합니다.
- useEffect에서 50ms마다 한 글자씩 추가하는 타이머를 설정합니다.
- 텍스트가 모두 표시되면 1초 후에 다음 텍스트로 넘어갑니다.
- 모든 텍스트가 표시되면 타이핑 효과가 종료됩니다.
https://ko.react.dev/reference/react-dom/createPortal
https://xionwcfm.tistory.com/316