본문 바로가기

개발자 강화/프론트엔드

[개발] Rive 도입 이유 + 사용 경험

내가 현재 개발 중인 프로덕트는 rive를 쓰고 있다

 

Rive가 무엇인가?

https://rive.app/

 

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

728x90