๊ธฐ์กด ํ๋ก์ ํธ์ ํ์ด์ง ์ ํ ์ ๋๋ฉ์ด์
์ ๋ฃ์ผ๋ฉด์ ๋ง์ฃผ์ณค๋ ์ธ๋ชจ์๋ ์ฐ๋นํ์ ๊ธฐ๋ก. ์๋ ์ฝ๋ ๋ณด๋๋ฐ ๊ทธ๋ฅ ์๋ฌด์๊ฐ์์ด ๋ฉ์ฒญํ๊ฒ ์๊ฐํด์ ๋์จ ๋ฌธ์ ๊ฐ ๋๋ถ๋ถ์ด์๋ค. ๊ทธ๋๋.. ์ผ๊ธฐ๊ฐ์ ๋๋์ฐ๋ก ์ ์ด๋ด๋ ค๊ฐ๊ฒ ์ต๋๋ค. ๊ธฐ๋ณธ์ ์ธ ๊ฐ๋
๊ณผ ๋ฐฉ๋ฒ์ ์ด์ ๊ธ์ ์ ์ ๋ฆฌํด ๋์๋ค.
[๋ฑ
ํค์ฆ] 6. React transition group ๋ผ์ฐํ
ํธ๋์ง์
(1) - ๋์
ํ๊ธฐ
๊ธฐ์กด์ ๋ผ์ฐํ ๊ตฌ์กฐ ๋๋ฌธ์ ์๊ฒผ๋ ๋ฌธ์
์ฒ์ ํธ๋์ง์
๊ทธ๋ฃน์ ๋์
ํ์ ๋์ ์ฝ๋์ด๋ค. ํธ๋์ง์
๊ทธ๋ฃน ๊ด๋ จ ๋ก์ง์ ๋ฐ๋ก ๋ถ๋ฆฌ๋ฅผ ํ์ง ์์์๋ค. App.tsx
์ ๊ทธ๋๋ก ์ฝ์
ํ๋ฉด ์ฝ๋๊ฐ ๋๋ฌด ๊ธธ์ด์ง๋ ๋ฏํ ๋๋์ ๋ฐ์์ด์, ๊ฐ๊ฐ ๋ผ์ฐํ
๊ด๋ จ ์ปดํฌ๋ํธ์ ๊ฐ๋ณ์ ์ผ๋ก ํธ๋์ง์
์ ์ ์ฉํ๋ ค ํ์๋ค.
๊ทธ๋ฌ๋๋ ์ด๋ฐ ๋ฌธ์ ๊ฐ ์๊ฒผ์.
'๋๊ธธ ๊ฑท๊ธฐ'์์ ๊ฑท๊ณ ์๋ ๋๊ธธ์ด ์์ ๋ '๋๊ธธ ๊ณ์ฝํ๊ธฐ'๋ก ๋์ด๊ฐ ๋ ์ ๋๋ฉ์ด์ ์ด ์ ์ฉ๋์ง ์์๋ค. ๋๊ฒ ๋น์ฐํ๊ฑฐ์๋ค. ๊ฐ๊ฐ ๋ผ์ฐํฐ๋ง๋ค ํธ๋์ง์ ๊ทธ๋ฃน์ ์ ์ฉํ๋ ค๊ณ ํ๊ณ , '/walk'๊ณผ '/create'๋ ๋ค๋ฅธ ๋ผ์ฐํฐ์ ์์๋ค.
ServiceRouter.tsx
const ServiceRouter = () => {
const location = useLocation();
return (
<Wrapper>
<RouteTransition location={location}>
<Routes location={location}>
<Route path="/*" element={<HomeRouter location={location} />} />
<Route path="/walk/*" element={<WalkRouter />} />
<Route
path="/mypage/*"
element={<MypageRouter location={location} />}
/>
<Route path="/interest/*" element={<InterestRouter />} />
</Routes>
</RouteTransition>
</Wrapper>
);
};
์ด๋ ๊ฒ ์๋น์ค ๊ด๋ จ ๋ผ์ฐํฐ๋ฅผ ๋ฐ๋ก ๋ถ๋ฆฌํ๊ณ , ํธ๋์ง์ ๊ด๋ จ ์ปดํฌ๋ํธ๋ฅผ ๋ฐ๋ก ๋ถ๋ฆฌํ๋ค.
๊ทธ๋ฌ๊ณ ์๊ธด ๋๋ค๋ฅธ ๋ฌธ์ .
ํ์ด์ง ์์์ ๋ฐ๋ผ ๋ค๋ฅธ ์ ๋๋ฉ์ด์ ์ ๋ณด์ฌ์ฃผ๊ธฐ
ํญ๋ฐ์ ์๋ ์ธ๊ฐ์ ๋ฉ์ธ ํ์ด์ง๋ฅผ ์๋ค๊ฐ๋ค ํ ๋๋ ์๊น ์ ์ฉํ๋ ์ ๋๋ฉ์ด์ ์ด ๋ํ๋ฌ๋ค. ์ด๋ค ์์น์ ์๋ ๋ฒํผ์ ๋๋ฅด๋ ์ง ์ค๋ฅธ์ชฝ์์ ์ผ์ชฝ์ผ๋ก ๋์ด๊ฐ๋ ์ ๋๋ฉ์ด์ ์ผ๋ก ๋ณด์ธ๋ค. ์ง๊ธ ์์น์์ ์ผ์ชฝ์ ์๋ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ผ์ชฝ์ผ๋ก ์ฌ๋ผ์ด๋ํ๊ณ , ์ค๋ฅธ์ชฝ์ ์๋ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ค๋ฅธ์ชฝ์ผ๋ก ์ฌ๋ผ์ด๋ํ๋๋ก ํด๋ณด์.
RouteTransition.tsx
const pageOrder = ['/interest', '/', '/walk', '/mypage'];
const RouteTransition = ({ location, children }: RouteTransitionProps) => {
const pathname = location.pathname;
const state = location.state;
return (
<TransitionGroup
className={'transition-wrapper'}
childFactory={(child) => {
if (!state?.prev) {
return React.cloneElement(child, {
classNames: location.state?.direction || 'navigate-push',
});
} else {
if (pageOrder.indexOf(pathname) > pageOrder.indexOf(state.prev)) {
return React.cloneElement(child, {
classNames: 'slide-next',
});
} else {
return React.cloneElement(child, {
classNames: 'slide-prev',
});
}
}
}}
>
<CSSTransition exact key={pathname} timeout={300}>
{children}
</CSSTransition>
</TransitionGroup>
);
};
export default RouteTransition;
๋จผ์ ํ์ด์ง์ ์์๋ฅผ ๋ฐฐ์ด๋ก ์ง์ ํด๋์๋ค. ๋ถ๋ชจ์ ์๋
์ผ๋ ํญ๋ฐ์ ๊ตฌ์ฑ์ด ์กฐ๊ธ ๋ค๋ฅด์ง๋ง ๋คํํ ๊ฒน์น๋ ๋ถ๋ถ์ด ์์ด์ ํ๋๋ก ๊ด๋ฆฌํ ์ ์์๋ค. navigate์ state
๋ก ๋ฐ์ ๊ฐ๊ณผ pathname
์ ํ์ด์ง ์์๋ฅผ ๋น๊ตํด์ ๊ทธ์ ๋ง๋ ํด๋์ค๋ก ์ง์ ํด์ฃผ๋ ๋ฐฉ์์ด๋ค.
ChildFactory
์ ๊ฐ๊ฐ์ ์กฐ๊ฑด๋ง๋ค ๋ค๋ฅธ className์ ๋ฃ์ด์ค cloneElement
๋ฅผ ๋ฐํํ๋ ํจ์๋ฅผ ๋ฃ์ด์ฃผ์๋ค.
<NavLink to="/mypage" state={{ prev: pathname }}>
<Mypage fill={pathname === '/mypage' ? active[1] : active[0]} />
</NavLink>
์ด๋ฐ์์ผ๋ก Link
์ปดํฌ๋ํธ์ state๋ฅผ ๋ฌ์์ ๋ผ์ฐํ
์ ๋๊ฒจ์ค ์ ์๋ค.
Transiton.css
.slide-next-enter {
transform: translateX(100%);
}
.slide-next-enter-active {
transform: translateX(0);
transition: transform 300ms ease-in-out;
}
.slide-next-exit {
transform: translateX(0);
}
.slide-next-exit-active {
transform: translateX(-100%);
transition: transform 300ms ease-in-out;
}
.slide-prev-enter {
transform: translateX(-100%);
}
.slide-prev-enter-active {
transform: translateX(0);
transition: transform 300ms ease-in-out;
}
.slide-prev-exit {
transform: translateX(0);
}
.slide-prev-exit-active {
transform: translateX(100%);
transition: transform 300ms ease-in-out;
}
๊ทผ๋ฐ ๊ทธ๋ฅ ์ฌ๊ธฐ ์ ๋๋ฉ์ด์ ์ ๋นผ๋ฒ๋ ธ๋ค. ๋งค๋ฒ ์ ๋๋ฉ์ด์ ์ด ๋ค์ด๊ฐ๋ ๋ฌด์ธ๊ฐ ๋ฒ์กํด์ง๊ณ , ์๋ ์ฌ์ด์์ ์์ง์ผ๋ ๊ฐ์ด๋ฐ ํ์ด์ง๋ฅผ ๊ฑด๋๋ฐ๊ณ ๋ฐ๋ก ํธ๋์ง์ ์ด ๋๋๊ฒ ์กฐ๊ธ ์ด์ํ์. ๋ค๋ฅธ ๋ง์ ์ฑ๋ค์ ๋ณด์์ ๋, ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ํญ๋ฐ ๊ฐ ์ด๋์๋ ์ ๋๋ฉ์ด์ ์ด ์๊ธฐ๋ ํ๋ค.
๋ผ์ฐํ ์ด ์๋, state ๋ณ๊ฒฝ์ ๋ฐ๋ฅธ ์ ๋๋ฉ์ด์ ๋ฃ๊ธฐ
๋๊ธธ ๊ณ์ฝํ๊ธฐ ๊ณผ์ ๊ณผ ์๋น์ค ์ด์ฉ ๋ฐฉ๋ฒ ๋ฑ์ ์ฌ์ฉํ๋ค. ํ์ด์ง๋ ๊ณ ์ ๋๊ณ ๊ทธ ์์์ ๋ด๋ถ ์ปดํฌ๋ํธ์ ํธ๋์ง์ ์ ์ฃผ๊ณ ์ถ๋ค. ๋ง๋ก ์ฐ๋๊น ์ดํด๊ฐ ์ ์๋ผ์, ๊ฒฐ๊ณผ ๋จผ์ .
๊ฝค ์ด์๋ค.
SlideTranstion.tsx
const SlideTransition = ({
keyValue,
direction,
children,
}: SlideTransitionProps) => {
return (
<TransitionGroup
style={{ position: 'relative' }}
childFactory={(child) => {
return React.cloneElement(child, {
classNames: `slide-${direction}`,
});
}}
>
<CSSTransition
key={keyValue}
timeout={300}
classNames={`slide-${direction}`}
>
{children}
</CSSTransition>
</TransitionGroup>
);
};
export default SlideTransition;
๋ณ๋ก ๋ค๋ฅผ ๊ฒ ์๋ค. direction๊ณผ key๋ฅผ props๋ก ๋ฐ์์ ๋ฃ์ด์ค๋ค.
Create.tsx
function Create() {
const [step, setStep] = useState<TStep>(1);
const [direction, setDirection] = useState<'next' | 'prev'>('next');
const onPrevButtonClick = () => {
setDirection('prev');
setStep((step - 1) as TStep);
};
const onNextButtonClick = () => {
setDirection('next');
setStep((step + 1) as TStep);
};
return (
<ForegroundTemplate
label="๋๊ธธ ๊ณ์ฝํ๊ธฐ"
customEvent={step !== 1 ? onPrevButtonClick : undefined}
to="/"
>
<Wrapper>
<SlideTransition keyValue={step} direction={direction}>
<ContentWrapper>
// ...content
</ContentWrapper>
</SlideTransition>
</Wrapper>
</ForegroundTemplate>
);
}
export default Create;
์ ๋นํ ๋์ด์ ๊ฐ์ ธ์๋ค. '๋ค์์ผ๋ก ๊ฐ๊ธฐ' ๋๋ '์ด์ ' ๋ฒํผ์ ๋๋ฅผ ๋ step ์ํ์ ํจ๊ป direction ์ํ๋ฅผ ๋ฐ๊ฟ์ค๋ค. ๊ฐ๊ฐ key์ direction props๋ก ๋๊ธฐ๋ฉด ๋๋ค. TransitionGroup
์์ key ๊ฐ์ด ๋ฐ๋๋๊ฑธ ๊ฐ์งํ๋ฉด ํธ๋์ง์
์ ๋ง๋ค์ด ์ฃผ๋ ๊ฒ.
step1์ผ ๋ ๋ค๋ก๊ฐ๊ธฐ๋ฅผ ๋๋ฅด๋ฉด ์ฌ๋ผ์ด๋ ์ ๋๋ฉ์ด์ ์ด ์๋๋ผ ๋ผ์ฐํ ์ ๋๋ฉ์ด์ (navigate-pop)์ด ๋์์ผ ํ๋ค.
const onClickAppBar = () => {
if (customEvent) {
customEvent();
} else {
to
? navigate(to, {
state: { direction: 'navigate-pop' },
})
: navigate(-1);
}
};
์ฑ๋ฐ ๋ค๋ก๊ฐ๊ธฐ๋ฒํผ์ onClick์ ๋ค์ด๊ฐ๋ ํจ์์ด๋ค. customEvent
props์ด ์์ผ๋ฉด ๊ทธ๊ฑธ ์ํํ๊ณ , ์์๋ navigate ์ ๋๋ฉ์ด์
์ด ์ผ์ด๋๋๋ก ํ๋ค.
๋จ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ง ์ ์ฉํ๋ฉด ๋๋ ์ค ์์๋๋ฐ ์๊ฐ๋ณด๋ค ์ฑ๊ฒจ์ผ ํ ๊ฒ ๋ง๋ค.
๊ทธ๋๋ ์ด์ , ๊ฐ์ชฝ๊ฐ์ด ์ฑ์ฒ๋ผ ๋์ํ๋ค!!