이 글은 리액트 / 리액트 네이티브 프로젝트에서 적용할 수 있는 가장 간단한 레이아웃, 인터랙션에 대한 가이드를 담고 있어요 :)
요즘 프론트엔드 채용공고를 보면 UI/UX 개선 역량이 필수로 요구되는 경우가 많아요. 실무에서는 간단한 개선도 기획자, 디자이너와의 소통이 필요할 때가 많죠. 불필요한 소통을 줄이고, 빠르게 개선 효과를 내고 싶다면 저는 인터랙션을 적용하는 것을 추천드려요. ☺️
인터랙션은 단순해 보여도, 여러 요구사항을 반영하다 보면 코드가 복잡해지기 쉬워요. 다행히 리액트/리액트 네이티브 환경에서는 이런 불편함을 해소해주는 다양한 라이브러리가 있어요.
이 글에선 Motion 과 Moti 를 통해 아주 간단한 방법으로 인터랙션을 적용할 수 있는 방법을 알려드리려고 해요 :)

{step === 1 && (
<span className='step1'>
가장 쉽게 돈벌기!
</span>
)}
...
이현재 step 에 따라 간단한 문장을 보여주는 화면과 코드에요. 프론트엔드 개발을 하다보면 가장 흔하게 마주하게되는 UI 에요. 가장 기본적인 UI 이지만, 모션을 사용하면 아주 쉽게 생동감있게 표현할 수 있어요.

{step === 1 && (
// css 작성해주듯이 속성을 props 로 넘겨주기!
<motion.span className='step1' initial={{ opacity: 0, y: '10%' }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}>
가장 쉽게 돈벌기!
</motion.span>
)}
{step === 2 && (
// 속성을 미리 정의해서 공용 속성으로 사용하기!
<motion.span className='step2' {...SLIDE_RIGHT}>
아래 내용을 읽어주세요.
</motion.span>
)}
...
Motion 은 이렇게 선언적으로 인터랙션을 적용할 수 있어요. 만약 반복되는 코드 작성 없이 여러 컴포넌트에 동일한 인터랙션을 적용하고 싶다면 initial / animate 값을 상수로 선언해 바로 적용 할 수 있어요.
디자이너, 기획자에게 '이 인터랙션 어떠세요 ?!' 라는 의견을 전달했을때, '자연스러운 움직임이 너무 좋은데, 문구가 사라질 때 다음 문구가 등장하는 인터랙션이 자연스럽게 연결되면 좋을 것 같아요!' 라는 의견을 들을 수 있어요.
이 문제를 직접 해결하려면.. 어떻게 해야 할까요? 저라면 이렇게 구현했을 것 같아요.
이렇게 보면 별로 어려운 일은 아니에요. 하지만 언마운트가 지연된만큼 다음 컴포넌트의 마운트 시점까지 조절해야 레이아웃 시프트가 발생하지 않아요.

이럴땐 간단하게 Motion 의 AnimatePresence 를 사용하면 돼요. Motion 은 다음과 같은 강력한 기능들을 제공하고 있어요.
useIsPresent / usePresence 와 같은 훅으로 현재 컴포넌트가 언마운트 되기 전 애니메이션을 재생하고 있는지에 대한 상태를 전달받을 수 있어요.그 중 AnimatePresence 의 mode 를 통해 각 컴포넌트의 순차적인 렌더링을 구현할 수 있어요.
// wait 는 각 컴포넌트의 동기적인 렌더링을 지원해줘요.
<AnimatePresence mode='wait'>
{step === 1 && (
<motion.div key='step1' className='step1' ... exit={{ opacity: 0, x: '-10%' }}>
가장 쉽게 돈벌기!
</motion.div>
)}
...
</AnimatePresence>

이렇게 단순히 하나의 속성을 통해 자연스러운 인터랙션을 적용할 수 있어요. Motion 의 AnimatePresence 는 기본적으로 자식 요소의 key 값을 통해 마운트 / 언마운트 상태를 관리하기 때문에 key 값을 필수로 부여해줘야 해요.
텍스트, 컴포넌트를 자연스럽게 렌더링하는것도 어려운 과제 중 하나이지만, 자연스러운 레이아웃 인터랙션은 어떻게 구현해야할까요? 리액트 네이티브를 사용하는 앱 프로젝트에서는 네이티브 앱 같은 자연스러움을 구현하기 위해 레이아웃 인터랙션을 구현해야 하는 상황이 자주 발생하기도 해요.
리액트 네이티브에서는 웹과 비슷하지만 다르게 StyleSheet , Animated API 를 통해 스타일링 / 인터랙션을 적용할 수 있어요. 처음엔 Animated API 를 통해 여러 인터랙션을 적용하려 했지만 아래와 같은 문제가 있었어요.
이러한 문제를 해결하기 위해 Reanimated 를 사용했어요. Reanimated 는 다음과 같은 기능들을 제공해줘요.

제품개발을 하다보면 이렇게 리스트를 관리하거나, 배너 / 팝업 등을 연쇄적으로 노출하는 컴포넌트를 개발하게 돼요. 간단하게 화면을 만들어도 괜찮지만, 사용자 입장에서 요소 변경이 잦은 화면의 레이아웃이 부자연스럽게 변경되는게 UX 측면에서 피로감을 줄 수 있다고 생각했었어요.
이러한 문제를 개선하기 위해서 직접 레이아웃 인터랙션 코드를 작성하려면 이러한 부분들을 고려해야해요.
Reanimated 는 이러한 문제를 쉽게 해결할 수 있도록 Layout Transition 프리셋을 제공해줘요.

// 간단한 코드로 레이아웃 인터랙션을 적용할 수 있어요.
<Animated.View
...
key={props.id}
layout={LinearTransition.easing(Easing.bezierFn(0.25, 0.1, 0.25, 1.0))}
>
<Popup} />
</Animated.View>
이렇게 Reanimated 를 통해 아주 쉽게 레이아웃 인터랙션을 적용할 수 있어요. 이러한 인터랙션은 알림 내역, 리스트 추가, 팝업 열기 / 닫기 등 여러 상황에서 자연스러운 플로우를 구현 할 수 있도록 도와줘요.
제가 실제로 경험했던 일이에요. 간단한 애니메이션을 구현하려고 했지만 여러 애니메이션이 동시에 재생되거나 순차적으로 재생되도록 만들다보니 코드가 너무 복잡해졌었어요.
// 총 200줄 이상..!
const isClosing = useSharedValue<boolean>(false);
const animatedOpacityForContainer = useSharedValue<number>(0);
const animatedTranslateYForContent = useSharedValue<number>(0);
const animatedStyle = useAnimatedStyle(() => { ... }, []);
useAnimatedReaction( ...
[...],
);
정작 기능 하나를 실행하기 위해 소요되는 시간은 10초 남짓이었지만 코드는 몇백줄이 넘어가버렸어요. 여러 기능에 인터랙션을 적용하기 위해 계속 이렇게 코드를 작성하게될거라 생각하니 걱정이 되기 시작했어요.
처음엔 직접 이러한 문제를 해결하기위해 구현체를 직접 만들어 사용했지만, 직접 꾸준히 유지보수를 진행하면서 사용하기엔 완벽한 설계를 하기위한 시간이 부족하다 생각했었어요.
그래서 저는 오픈소스의 이점을 활용하기로 했고, 적절한 라이브러리를 발견했어요!
Moti 는 Reanimated 기반으로 제작되어 웹용 라이브러리인 Motion 과 매우 유사한 인터페이스를 갖고있어요. 간단한 인터페이스를 통해 인터랙션을 적용할 수 있도록 도와주고, 웹 / 앱을 함께 개발하는 모노레포 환경에서 인터랙션을 적용하기위한 인터페이스 간극을 조금이라도 줄이면서 개발할 수 있도록 도와줘요.
// Reanimated 사용..
const sharedOpacity = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => {
return {
opacity: sharedOpacity.value,
};
});
useEffect(() => {
opacity.value = withSequence(
withTiming(1, { duration: 100 }),
withTiming(0, { duration: 100 }),
);
,[]};
return (
<Animated.View style={{opacity}}>
...
// Moti 사용
return (
<MotiView
key={props.id}
from={{opacity:0}}
animate={{opacity:1}}
>
...
</MotiView>
);
Reanimated 를 사용할땐 비교적 덜 중요한 스타일링 로직이 비즈니스 로직과 섞일 수 있는 반면, Moti 를 사용하면 스타일링 관련 코드는 아래로 내리면서 짧은 코드로 동일한 인터랙션을 적용할 수 있어요.
Reanimated 를 사용해서 Motion 의 퇴장 애니메이션을 선언적으로 구현하기 위해선 별도의 Props 를 부여하거나 sharedValue 를 정의해 또 다른 로직을 작성해야 할 수 있어요. Moti 를 사용하면 Moti 의 AnimatePresence 를 사용해 동기적인 렌더링, 퇴장 인터랙션을 재생 할 수 있어요.
저는 이렇게 200줄이 넘어가는 코드를 약 10줄로 줄인 경험이 있어요. 🙄
Moti 는 Reanimated 기반으로 개발되었기 때문에 Reanimated 가 제공하는 기본적인 기능들을 모두 사용할 수 있다는 장점도 있어요 :)
저는 이렇게 어렵지 않지만 변화가 작지 않은 인터랙션 적용을 통해 동료들에게 좋은 피드백을 들을 수 있었어요. 이렇게 하나하나 적용해나가다 보니 제가 개발하는 서비스가 점점 자연스러운 흐름을 갖추게 되더라구요. 제가 할 수 있었던 것들중에 가장 간단했지만 가장 큰 효과를 주지 않았나 생각해요!
이러한 기능을 직접 구현하려면 고려해야 할 점이 많고, 잘못된 설계는 유지보수를 어렵게 만들 수 있어요. 이럴땐 빠르게 적절한 라이브러리를 찾아 문제를 해결하는것도 해결 방법 중 하나라고 생각해요 :)