내가 현재 개발 중인 프로덕트는 rive를 쓰고 있다
Rive가 무엇인가?
Rive — a new way to design, build, and ship user interfaces
Rive combines an interactive design tool, a new stateful graphics format, a lightweight multi-platform runtime, and a blazing-fast renderer. This pipeline brings interfaces to life with motion across apps, games, websites, products, and vehicles.
rive.app
(공식 사이트)
상태 머신(state machine) 기반 인터랙티브 벡터 애니메이션 툴
- 애니메이션 간의 전환, 사용자의 입력(클릭, 호버, 스크롤 등) 혹은 데이터 변화에 따라 state가 변함
- state 변화에 따라 애니메이션이 진행되도록 설계된 state machine 인터페이스를 가짐
디자니어와 개발자가 같은 파일을 공유해 실시간 반응형 애니메이션을 구현할 수 있게 해줌
- 디자이너가 만든 애니메이션을 개발자가 별도로 다시 애니메이션화 하거나 코드화하지 않아도 됨
- 디자인 툴과 코드 통합의 갭을 좁힐 수 있음
벡터 기반 & 고성능 렌더링
- 벡터 그래픽 기반으로 작동하며, 복잡한 애니메이션이라도 파일 크기가 작게 유지되며 고성능으로 실행될 수 있음
- 120fps까지 지원하고, 저사양 기기에서도 무리 없이 실행됨
+ 디자이너님께서는 주로 게임에서 자주 쓰인다고 하심
Rive를 왜 도입했는가?
내가 입사했을 때는 이미 팀에서 Rive를 사용하고 있었다
노션과 슬랙을 역추적해보며 Rive 도입 배경을 살펴보았다
디자이너님의 입장에서
Rive 도입 이유
- 다채롭고 효율적인 모션 작업을 위해 Rive를 도입함
- 그동안 불가능했던 효과를 사용하는 것이 가능했짐
- 모션 작업의 복잡한 단계가 줄어 효율이 증가함
- 상태 변화에 따른 모션 작업 설정이 가능해 FE와 긴밀하고 정확한 소통이 가능해짐
(+ 같이 일하면서 디자이너님이 자주 말씀하셨던 것)
- png을 바로 업로드해서 만들 수 있음 (ai 생성 결과물을 바로 사용가능하다는 뜻)
- 벡터로 다시 하나하나 그리지 않아도 돼서 작업 속도가 매우 빨라짐
Rive 도입 과정에서 이슈
- input, state에 따른 트리거 구조를 고안하는게 어려움
- 개발자적인 사고방식이 필요함 (stateMachine 때문인 듯)
FE 개발자(전임자) 입장에서
Rive 도입 과정에서 이슈
- 러닝 커브
- advanced한 기능을 파악하지 않은 상태로 작업했더니 최적화를 신경쓰지 못함
- 연산량 이슈
- rive 파일에서 svg 타입의 그림을 움직이는 애니메이션은 연산량이 큼
- svg로 Rive를 만든 경우와 png로 Rive를 만든 경우 cpu 온도가 10~15도 가량 차이남
FE 개발자(본인) 입장에서
Rive 사용 시 좋았던 점
- 디자이너의 결과물 만족도
- motion.div로 state별 애니메이션을 구현할 수 있지만, 한계가 있음 (이미지 원본에 크게 의존함)
- 디자이너가 원하는 형태의 애니메이션을 rive로 직접 구현하고, 개발자는 이를 정확한 시점에 트리거 하는 형태로 작업
- 업무 배분면에서 긍정적
- 일부 기능은 개발자가 motion.div로, 일부 기능은 디자이너가 rive로 구현.
- motion.div로 구현한 부분만 디자이너의 피드백을 반영하면 됨
- 개발 과정이 재밌었음
- 개인적으로 디자이너와 완전 긴밀한 협업하는 것을 좋아함(uiux 챙기는 걸 좋아함)
- rive를 사용할 때 디자이너가 설계한 stateMachine의 의도를 묻고, 실시간으로 구현 결과물을 피드백 받음
- 이 과정 자체가 재밌었음
Rive 사용 시 아쉬웠던 점
- 레퍼런스
- 사용 사례가 아직 많지 않아 매 케이스에 적절한 레퍼런스를 찾기 쉽지 않음
- ai에게도 다른 라이브러리에 비해 적절한 답변을 얻기 어려운 편이었음
- 러닝 커브
- 입사 후 6일 후에 prod에 신기능을 출시해야 했는데, 그 기간 사이 rive+relay를 익히는 게 꽤 어려웠음
Rive, 그래서 어떻게 쓰는가?
Rive를 도입하셨던 분들이 하나 둘 떠나고...
내가 Rive 사용법을 테크니컬 문서로 만들어서 공유함
다만, 디자이너님께서 넘겨주신 Rive 파일이 있다는 가정 하에 작성함
Rive 도입을 원한다면 팀의 디자이너님과 논의를 해서 Rive 파일을 만드는 것부터 시작해야 함
How to?
의외로 기본 세팅 자체는 매우 간단함. 복잡한 트리거를 구현해야할 때부터 좀 어려워짐
[1] Rive 라이브러리를 레포에 설치한다
https://www.npmjs.com/package/@rive-app/canvas
본인 레포에서 사용하는 패키지 관리 도구 명령어로 @rive-app/canvas를 추가하면 됨
[2] Rive 파일을 다운받아 asset 폴더에 넣는다
.riv는 클라이언트에 집어넣을 수 있도록 추출한 파일
.rev는 Rive 에디터로 편집이 가능한 파일
.riv를 추출한 후 .rev의 행방을 잃지 않게 조심하세요...
.riv만 있으면 더 이상 수정사항을 반영할 수 없음...
덮어 씌우기와 버저닝에 유의하세요... (모든 디자인 파일이 그렇지만...)
[3] Rive 파일을 실행하는 코드를 작성한다
사실 여기가 관건! 아래 실행 코드 예시를 참고
Rive 실행 코드 예시
가장 기본 형태: Rive를 넣어서 표시만 하는 경우
import { useRive } from '@rive-app/react-canvas';
/// 중략
const { RiveComponent: 커스텀라이브컴포넌트이름 } =
useRive({
src: 'Rive 파일이 위치한 경로',
artboard: '디자이너님께서 정의한 artboard 이름',
stateMachines: '디자이너님께서 정의한 stateMachine명',
autoplay: true,
useOffscreenRenderer: true,
});
/// 중략
return (
/// 중략
<커스텀라이브컴포넌트이름 className='h-[15vw] w-[15vw]' /> // 꼭 사이즈를 지정해줘야 함
경험상 이슈: 사이즈 지정을 안하면 화면에 Rive 요소가 ui에서 안보임
1) autoplay 속성
타입: boolean, 기본값: true
Rive 애니메이션이 로드되자마자 자동으로 재생을 시작할지 여부를 결정함
- true: 컴포넌트가 마운트되면 애니메이션이 즉시 시작됨
- false: 애니메이션이 자동으로 시작되지 않으며, 수동으로 play() 메서드를 호출해야 함
2) useOffscreenRenderer
타입: boolean, 기본값: false
브라우저의 OffscreenCanvas API를 사용해 렌더링 여부를 결정
- true: Web Worker에서 렌더링을 처리해 메인 스레드의 부담을 줄임. 복잡한 애니메이션에서도 ui가 부드럽게 동작.
- false: 메인 스레드에서 렌더링을 처리함
장점
- 메인 스레드에서 렌더링 작업을 분리해 성능 향상
- 복잡한 애니메이션이나 여러 Rive 인스턴스를 동시에 사용할 때 유용함
주의사항
- 모든 브라우저가 OffscreenCanvas를 지원하는 건 아님(주로 최신 브라우저에서 지원)
- 지원하지 않는 브라우저에서는 자동으로 일반 렌더링으로 폴백됨
이와 관련한 경험적 이슈
useOffscreenCanvas: true 설정을 안해줬을 때(=false일 때), ui에 rive 컴포넌트가 안보임
claude에게 질문했을 때는 이런 답변을 줌
(1) false인 경우에는 일반 canvas를 사용함. 이때 canvas 내부 해상도가 제대로 설정 안되면 렌더링 안될 수 있음.
// 일반 Canvas는 크기 지정이 더 중요함
<커스텀라이브컴포넌트이름
className='h-[15vw] w-[15vw]'
style={{ width: '15vw', height: '15vw' }} // inline style도 함께
/>
경험상 크기 속성 미지정했을 때 rive가 ui에 안보이는 이슈를 자주 겪었어서, 이 가능성 꽤 높아보임
(2) 레이아웃/렌더링 타이밍 이슈
- OffscreenCanvas은 Web Worker에서 독립적으로 렌더링되므로 타이밍 문제에 더 강함
- 일반 canvas는 DOM이 완전히 준비된 후 렌더링되어야 하는데, React 렌더링 사이클과 안맞을 수 있음
⬆️이거 무슨 말인지 이해 안돼서 claude한테 더 설명해달라 함
React 렌더링 사이클
1. Render Phase (렌더 단계)
- React가 JSX를 실행하고 Virtual DOM 생성
- 아직 실제 DOM에는 아무것도 없음
2. Commit Phase (커밋 단계)
- Virtual DOM을 실제 DOM에 반영
- <canvas> 요소가 실제로 DOM에 추가됨
3. useEffect 실행 - DOM이 완전히 준비된 후 실행
일반 canvas 문제점
const { RiveComponent } = useRive({
src: 'animation.riv',
useOffscreenRenderer: false, // 일반 Canvas 사용
});
// React가 이 컴포넌트를 렌더링할 때:
return <RiveComponent className='h-[15vw] w-[15vw]' />
문제 발생 시나리오:
1단계: React Render Phase
└─> useRive 훅이 실행됨
└─> Rive 라이브러리가 Canvas를 초기화하려고 시도
└─> 🚨 하지만 아직 실제 <canvas> DOM 요소가 없음!
2단계: React Commit Phase
└─> <canvas> 요소가 DOM에 추가됨
└─> 하지만 Rive는 이미 초기화를 시도했고 실패함
3단계: useEffect
└─> 너무 늦음, Canvas 컨텍스트 획득 실패
OffscreenCanvas가 이 문제를 해결하는 방법
const { RiveComponent } = useRive({
src: 'animation.riv',
useOffscreenRenderer: true, // ✅ OffscreenCanvas 사용
});
OffscreenCanvas의 동작:
1단계: useRive 실행
└─> OffscreenCanvas 생성 (DOM 없이도 생성 가능!)
└─> Web Worker로 전송
└─> Worker에서 독립적으로 렌더링 시작
2단계: React Commit Phase
└─> 실제 <canvas> DOM 요소 생성
└─> OffscreenCanvas와 연결 (transferControlToOffscreen)
3단계: 렌더링
└─> Worker가 OffscreenCanvas에 그림
└─> 결과가 자동으로 실제 Canvas에 표시됨
이것도 가능성 있어보임
왜냐면 rive에 input으로 특정 state를 trigger로 할 때
rive cannot read properties of null 라는 이슈가 자주 발생했기 때문임
이에 대한 설명은 아래 trigger 케이스에서 이어서 설명...
액션 트리거: 단순 trigger 케이스
import { useRive, useStateMachineInput } from '@rive-app/react-canvas';
/// 중략
const { rive: 커스텀라이브훅이름, RiveComponent: 커스텀라이브컴포넌트이름 } =
useRive({
src: 'Rive 파일이 위치한 경로',
artboard: '디자이너님께서 정의한 artboard 이름',
stateMachines: '디자이너님께서 정의한 stateMachine명',
autoplay: true,
useOffscreenRenderer: true,
});
// useStateMachineInput으로 trigger를 선언
const exampleInput = useStateMachineInput(
커스텀라이브훅이름,
'디자이너님께서 정의한 stateMachine명',
'디자이너님께서 정의한 input명',
);
useEffect(() => {
// 어떤 조건에서 애니메이션을 트리거할지
if (애니메이션 트리거 조건) {
try {
exampleInput.fire(); // fire = 트리거 작동
} catch (error) {
// RiveComponent 렌더링 전 input을 fire 시도하는 경우 에러 발생해서 방지용으로 넣음
}
}
}, [exampleInput]);
useEffect 안에 try-catch문을 집어넣었는데, 이는 rive cannot read properties of null 에러 때문임
트리거 포함된 rive를 추가하고 나면 센트리에 에러가 급격히 늘어나는데,
null의 value값을 읽을 수 없다! 이런 내용의 에러임
claude 분석에 의하면 위에서 렌더링 타이밍 이슈에 관한 내용이 나왔는데,
그 이슈가 이와 관련되어 있을수도 있다는 생각이 듦
const { rive: 커스텀라이브훅이름, RiveComponent } = useRive({...});
const exampleInput = useStateMachineInput(
커스텀라이브훅이름, // 🚨 초기에는 null!
'stateMachine명',
'input명',
);
useEffect(() => {
exampleInput?.fire(); // 🚨 exampleInput이 null일 수 있음
}, [exampleInput]);
타이밍 순서:
1. 첫 렌더: useRive 실행
└─> rive = null (아직 로딩 중)
└─> RiveComponent 생성됨
2. 첫 렌더: useStateMachineInput 실행
└─> rive가 null이므로
└─> exampleInput = null
3. 첫 렌더: useEffect 실행
└─> exampleInput.fire() 호출
└─> 🚨 에러! "Cannot read properties of null"
4. Rive 파일 로딩 완료
└─> rive가 업데이트됨
└─> 리렌더링 발생
5. 두 번째 렌더: useStateMachineInput 실행
└─> 이제 rive가 존재함
└─> exampleInput = 실제 input 객체 ✅
내가 예상했던 사이클과 비슷하게 분석을 했는데,
이를 해결하기 위해서 옵셔널 체이닝도 써보고 (input?.fire()), if문 조건에 rive 훅도 넣어봤는데
그 어느것도 효능이 없었음
결국 마음에 드는 방법은 아니지만 try-catch문으로 감쌌을 때는 화면터짐 이슈도 해결되고 에러도 안잡혔음
구글링해도 이런 케이스가 없는 건 아닌데 뾰족한 해결책을 못찾음...
아시는 분은 공유부탁드립니다ㅠ
액션 트리거: value 할당 후 trigger
가위바위보 아케이드를 구현하면서 만들었던 코드를 공개 가능하게 general하게 바꾼 예시
import { useRive, useStateMachineInput } from '@rive-app/react-canvas';
/// 중략
const { rive: 커스텀라이브훅이름, RiveComponent: 커스텀라이브컴포넌트이름 } =
useRive({
src: 'Rive 파일이 위치한 경로',
artboard: '디자이너님께서 정의한 artboard 이름',
stateMachines: '디자이너님께서 정의한 stateMachine명',
autoplay: true,
useOffscreenRenderer: true,
});
// useStateMachineInput으로 Boolean/Number input을 선언
const exampleBooleanInput = useStateMachineInput(
커스텀라이브훅이름,
'디자이너님께서 정의한 stateMachine명',
'디자이너님께서 정의한 Boolean input명',
);
useEffect(() => {
// Boolean 값을 가진 input의 경우 value 할당 후 fire
if (애니메이션 트리거 조건) {
try {
exampleBooleanInput.value = true; // 또는 false
exampleBooleanInput.fire(); // value 설정 후 트리거
} catch (error) {
// RiveComponent 렌더링 전 input을 fire 시도하는 경우 에러 발생해서
방지용으로 넣음
}
}
}, [exampleBooleanInput]);
아래 표는 트리거 액션에 대한 케이스를 간단히 표로 정리해본 것이다
Input 타입 | 사용 방법 | 설명 |
Trigger | input.fire() | 단발성 이벤트 (예: 버튼 클릭) |
Boolean | input.value = true/false input.fire() |
상태 on/off (예: 토글, 활성화 여부) |
Number | input.value = 숫자 input.fire() |
수치 전달 (예: 진행도) |
실제 작업 과정?
1. 디자이너님께 stateMachine 설명을 부탁드린다
- 이게 제일 중요한 절차라 생각함
- 입사 직후 처음에는 '이렇게 작동하는건가?'라고 생각하면서 우당탕탕 했는데, 알고보니까 디자인 의도랑 다를 때가 많았음.
- stateMachine의 구조와 의도를 디자이너/FE가 서로 완전히 align하고 작업을 시작하는 게 리소스 낭비 방지에 굉장히 중요함
2. 디자인 의도에 맞추어 코드 구조를 설계한다
- 어떤 시점에 어떤 value를 할당하고, 어떤 input을 트리거할 건지 설계하는 과정이 생각보다 중요했음
3. 코드를 작성하고, 디자이너에게 실시간 uiux 피드백을 받으면서 수정한다
- 이때만큼은 본인이 fe개발자가 아니라 디자인 툴 개발자가 됐다는 생각이 듦
- 서로 합의 가능한 결과가 나올 때까지 수정을 반복한다
- 주로 트리거 작동 시점이 관건이었음 (코드로 원하는 트리거 시점이 정확히 안나올 때가 많았음)
이상 rive 사용 후기를 마칩니다~
또 추가할 사항이 있다면 후속 글을 들고 오겠습니다.
2025.10.19. 일 01:31
'개발자 강화 > 프론트엔드' 카테고리의 다른 글
[개발] Relay(GraphQL) 사용 경험 (0) | 2025.10.23 |
---|---|
[개발] framer motion (motion.div) 사용 경험 (0) | 2025.10.21 |
[개발] FECONF25 로띠 세션에서 알게 된 닷로띠를 실무에 적용하기 (0) | 2025.10.09 |
[개발] 의외로 주니어가 할 수 있는 것: 문서화 문화 도입 (0) | 2025.10.08 |
[매일메일+개발] 타입스크립트 사용하는 이유? + 실무 경험담 (0) | 2025.10.06 |