세 플랫폼(web, ios, android)에서 모두 동작하는 Accordion 구현(expo)

Dev-Yuns
Cross-Platform Korea
8 min readAug 26, 2020

ExpoReact-native의 플랫폼으로서 여러가지 편리한 모듈과 환경을 제공함으로써 개발 편의성을 높여줍니다. 이번 포스트에서는 dooboo-ui라는 오픈 소스 라이브러리에 accordion 컴포넌트 제작 기여를 했던 경험을 소개해보겠습니다. dooboo-ui는 expo를 활용해 하나의 코드로 세 플랫폼(ios, android, web)에서 모두 잘 동작하는 UI Component를 만드는 것을 목표로 합니다.

결과물은 아래와 같습니다. web뿐만 아니라 ios, android에서 잘 동작합니다.

사이드 메뉴바나 Q&A화면으로 활용할 수 있는 accordion
Android 버전
IOS 버전

Animated VS LayoutAnimation

React native에서 애니메이션은 Animated LayoutAnimation이 있습니다. 두 라이브러리의 차이는 Animated는 컴포넌트의 실제 레이아웃을 바꾸지는 않으며, 해당 컴포넌트를 변형하는 것처럼 보이는 것일 뿐입니다. 따라서 다른 컴포넌트에 영향을 주지 않습니다. 반면 LayoutAnimation은 말 그대로 레이아웃이 변화하는데 사용될 수 있습니다. 이외에도 AnimatedLayoutAnimation보다 세심하게 동작을 컨트롤 할 수 있습니다. LayoutAnimation에 대한 자세한 정보는 이 블로그를 참고하시길 바랍니다.

여기서는 Animated를 사용했으며, 부드러운 애니메이션 효과를 위해 useNativeDriver 메소드를 사용했습니다.

useNativeDriver

애니메이션은 값이 a에서 b로 바뀔 때, a가 연속성을 가지며 점점 b를 향해 가는 것이라고 할 수 있습니다. React native에서 애니메이션은 아래와 같은 과정을 통해 진행됩니다.

  1. 애니메이션이 시작되면 Animation driver는 내부적으로requestAnimationFrame을(초당 60회 호출 =60 frame) 호출
  2. JS Thread에서 애니메이션 대상이 되는 값을 계산
  3. 계산된 값은 serializes 되어 bridge를 통해 Native로 전달
  4. Native에서는 이 값을 받아서 UI 업데이트

위 단계에서 animated value는 state를 변경하지 않기 때문에 re-rendering은 일어나지 않게 됩니다.

문제는 초당 60 Frame 호출을 통해 부드럽게 애니메이션을 실행하려면 JS Thread는 제한된 시간 내에 빠르게 작업을 처리하고 Native로 값을 넘겨주어야 합니다. 만약 작업량이 많다면 애니메이션이 버벅거리거나 부드럽지 않을 수 있습니다. 이럴 때 useNativeDriver를 사용해주면 모바일에서 부드러운 애니메이션을 구현하는데 도움이 됩니다.(다만 Web은 지원하지 않습니다)

위 메소드를 사용하면 한번의 직렬화(serialize)로 Native에서 값을 받아서 대부분의 작업을 처리한 후, UI thread에서 직접 View를 업데이트 합니다. 더 자세한 내용은 문서를 확인하세요.

구현 과정

타입스크립트와 styled component를 사용했으며, react hook을 이용한 함수형 컴포넌트로 만들어져 있습니다 :)

기본 로직은 타이틀과 바디값을 담은 데이터를 accordion에 배열로 넣어주면 내부적으로 하나의 타이틀과 바디를 가진 accordionItem들을 맵핑합니다. 각 accodionItem은 다시 바디의 아이템들을 맵핑하게 됩니다.

Accoordion에 넣어줄 데이터

애니메이션을 구현하기 위해 원하는 컴포넌트를 Animted.View 로 감싸준 후, transform 을 통해 애니메이션이 적용될 수 있도록 합니다.

accordionItem의 바디부분을 감싸고 있는 부분

Height에 직접 애니메이션을 주지 않는 이유

부드러운 애니메이션 효과를 위해 useNativeDriver를 사용했기 때문입니다. 이 메서드를 사용하면 직접적으로 width나 hight와 같은 layout 프로퍼티에 적용할 수 없고, transform이나 opacity와 같은 non-layout 프로퍼티에만 적용할 수 있습니다.

The drawback of using ‘useNativeDriver’, is that it only supports things like transform and opacity. For example, we cannot use the native driver on an Animated.Value that is used directly on the “width” style of a view.

출처: https://medium.com/@robertoconnor94/using-usenativedriver-in-react-native-animations-effectively-7191287c6945

useEffect를 사용하여 애니메이션 효과 주기

애니메이션 효과

useState를 통해 opened라는 boolean값을 하나 만들어주고 클릭할 때마다 값이 바뀌도록 합니다. useEffect에서는 opened 의 변화를 감지해서 조건에 따라 애니메이션을 실행하게 됩니다.

Animated Value 넣어주기

일반적으로 Animated Value는 해당 컴포넌트에서 선언합니다. 하지만 Accordion 에서는 이런 방식에 문제가 있었습니다. 첫번째 accordionItem을 클릭 후, 애니메이션이 발생하기 위해서는 아래쪽의 accordionItem을 밀어내야 되기 때문입니다. 위에서 언급했듯이 Animated는 실제 레이아웃에 영향을 주지 않기 때문에 따로 아이템들을 아래로 밀어주는 작업이 필요했습니다.

이에 대한 해결책으로 accordionItem을 매핑하는 곳에서 item의 개수만큼 선언해준 후, 첫번째 item에는 자기의 value값을 가지도록 하고, 두번째 item에는 첫번째 item의 애니메이션 value와 함께 두번째 value를 넣어줍니다. 이처럼 다음 item으로 갈 수록 이전 item들의 값을 중첩하여 같이 넣어주었습니다. 중첩된 값은 sumOfPrecedingTranslateY 라는 props로 accordionItem으로 들어가서 전체 컴포넌트를 감싸고 있는 Animated.View 에 주입해줍니다.

이렇게 하니 첫번째 item에 애니메이션이 발생해서 아이템들이 내려올 때, 아래쪽 item들도 첫번째 item의 animation value 만큼 아래로 밀려나게 되었습니다.

AccordionItem을 맵핑하기 전에 animValue를 미리 선언해서 준비

dropDownAnimValueList 에서 accordionItem의 개수만큼 animation value를 준비하고, props로 순회하면서 각각 넣어주고 있습니다. 또한 sumOfPrecedingTranslateY에서는 맵의 index를 이용하여 앞서 순회한 item들의 animation Value들을 배열로 넣어주고 있습니다.

accordionItem 에서 가장 상위의 부모 컴포넌트

sumOfPrecedingTranslateY 는 accordionItem 내부에서 가장 상단에 있는Animated.Viewtransform 값으로 들어갑니다. 이때 style값으로 overflow: ‘hidden’ 도 꼭 넣어줍니다. 그래야 바디가 height: 0 으로 접혀있을 때 나타나지 않습니다.

글이 장황해지는 것을 막기위해 최대한 핵심적인 부분만 정리해보았습니다. 혹시 질문이 있으시거나 잘못된 부분이 발견된다면 언제든 댓글 부탁드리겠습니다. 감사합니다. 전체 코드는 아래 링크를 통해 확인하실 수 있습니다.

https://github.com/dooboolab/dooboo-ui/tree/master/main/Accordion

--

--