홈 화면 개발기

dudgus5766·2023년 1월 2일
2

Junior Developer

목록 보기
11/18
post-thumbnail

기존 앱 내에 홈(숏폼 피드 형식)에 대한 문제가 제기되면서 새로운 홈 화면으로 개편을 하게 되었습니다. 해당 홈 화면에 대한 전체 개발을 맡게 되었습니다 👨‍💻

전체적인 개발 과정, 고민, 문제 해결 방식에 대해 이야기해보려 합니다. 저의 생각과 고민으로 풀어낸 문제다 보니 부족한 부분이 많을 수 있으니 참고 부탁드립니다!


배경

Baund는 앱에서 제공하는 비트에 영상 컨텐츠 제작 및 공유할 수 있는 플랫폼입니다. 사용자의 유입과 기존 사용자들의 사용성을 늘리기 위해 홈화면을 개편하게 되었습니다.

영역 별 구조

1️⃣ 이벤트 캐러셀 영역

앱 내에서 진행 중인 이벤트 이미지를 캐러셀 형태로 보여줍니다.

2️⃣ 카테고리 탭 영역

기본적인 3가지 카테고리 탭을 보여주지만 이벤트성 카테고리 탭도 추가될 수 있기 때문에 동적으로 언제든지 바뀔 수 있는 가능성을 염두해두고 작업했습니다.

3️⃣ 탭 별 컴포넌트 영역

동적으로 컨텐츠들을 다르게 보여주다보니 공통 컴포넌트에 탭 별로 다른 api를 props로 내려주어서 각각 다른 컨텐츠의 UI를 보여주었습니다.
자세한 내용은 밑에서 이야기하도록 하겠습니다.

etc)검색 영역

돋보기 아이콘을 누르면 보여주는 화면입니다.


📍 캐러셀 애니메이션 만들기

홈 화면의 캐러셀은 react-native-reanimated-carousel 라이브러리를 사용하여 제작하였습니다.

좋은 사용자 경험을 위해 디자인 팀과 협의 후에 온라인 커머스 플랫폼 29cm무신사 앱 홈 화면에서 쓰는 애니메이션(parallax)을 적용하였습니다.

💡 parallax는 시차 라는 뜻으로, 멀리 있는 물체는 천천히 움직이고, 가까이 있는 물체는 빨리 움직이는 현상을 의미합니다.
기술적으로 parallax는 고정된 View에서 화면의 배경이미지 외의 다른 전경 요소들과 함께 스크롤로 이동할 때 배경이 바뀌도록 함으로써 얻을 수 있습니다.

라이브러리에서 animationStlye 메소드에 제공하는 value(캐러셀 x 값)를 z-indextranslateX 에 적용 후 커스텀하여 구현하였습니다.

/**
   * parallax 애니메이션
   */
const animationStyle = useCallback((value) => {
    const zIndex = interpolate(value, [-1, 0, 1], [10, 20, 30]);
    const translateX = interpolate(value, [Platform.OS === 'android' ? -1.5 : -2, 0, 1], [-아이템의 길이, 0, 아이템의 길이]);

    return {
      transform: [{ translateX }],
      zIndex,
    };
  }, []);

실제 화면


📍 컴포넌트 리팩토링

이번 홈 개편을 하면서 가장 신경쓰고 고민을 많이 했던 부분입니다. 기존 코드를 리팩토링하면서 UI 컴포넌트의 추상화와 재사용성에 공을 들여 개발하였습니다.

💡 컴포넌트란?
프론트엔드에서는 UI를 구성하고 있는 요소를 컴포넌트라고 할 수 있습니다. 반복되는 요소들을 매번 개발하기보다는 미리 만들어둔 요소(컴포넌트)를 재사용하여 여러 가지 이득을 취할 수 있기 때문에 컴포넌트로 개발합니다.

UI 기능 별로 컴포넌트 나누기

처음으로 피그마를 보면서 기획자, 디자이너, 담당 백엔드 개발자 분들과 함께 UI 별로 컴포넌트를 나눴습니다. 컨텐츠의 성격에 따라 컨텐츠 개수, title의 위치를 정했습니다.

기획을 검토하며 한 가지 문제가 있었는데, 서버에서 보내주는 같은 성질의 데이터 키값들이 달랐다는 것입니다. 예를 들어 비트를 다루는 서버 api의 비트 데이터 참조값들이 달라 어느 api는 beats 로, 또 어느 api는 recentlyBeat로 데이터가 넘어오는 것이었습니다. (서버팀과 회의 후에 업데이트 이 후 같은 목적으로 쓰이는 데이터는 모두 beats 로 통일하기로 얘기하고 수정을 부탁드리게 됐습니다..서버팀 감사합니다🥲)

이제 비트던, 플레이 article 이던 서버 api의 데이터 참조값들이 같아졌기 때문에 서버에서 보내주는 responseType 만으로 화면에서 보여주는 UI를 자유롭게 변경할 수 있게 되었습니다. 그래서 기존 비트 데이터의 UI 변경 또는 새로운 비트 데이터 UI 추가 시에도 비트와 관련된 responseType를 가진 UI 컴포넌트에 전부 적용할 수 있었습니다.

운영팀에서 편하게 데이터를 다루고 이해하기 쉽도록 notion에 각 탭에 나오는 레이아웃을 responseType으로 나눠 정리해두었습니다.

실제로 앱 업데이트 이후, ‘새로운 비트’ 라는 제목을 가진 비트 컨텐츠의 UI를 바꿔달라는 요청을 responseType 변경만으로 해결할 수 있었습니다.

또한 컴포넌트 상단의 제목더보기 버튼을 묶어 SectionTitle 컴포넌트로 분리하여 재사용성을 극대화하였습니다.

const SectionTitle = ({ title, btnText, onPress, viewMore }) => ...
const BeatCard = ({ item }) => ...

// 정방향의 큰 썸네일을 보여주는 UI 컴포넌트 
const HorizontalLargeList = (...) => {

return (
	<>
		<SectionTitle
			title={타이틀} // SectionTitle 내에서 title이 없을 경우 제목 섹션이 보이지 않음
			subTitle={서브타이틀}
			viewMore // 더보기 버튼이 필요한 경우 추가함
			btnText={t('common:view-more')}
			onPress={onPressMore}
		/>
		
		<BeatList
		   horizontal
		   snapToInterval={ITEM_WIDTH}
		   decelerationRate="fast"
		   showsHorizontalScrollIndicator={false}
		   data={data.result}
		   renderItem={({ item }) => ( <BeatCard item={item} />)}
		/>
		
</>)

...중략

📍 동적 컨텐츠 생성

Baund는 사용자들의 영상 뿐 아니라 이벤트, 유튜브 영상 등 다양한 컨텐츠로 이루어진 앱 입니다. 그렇기 때문에 코드를 변경하지 않고, 카테고리 탭 내 컨텐츠를 동적으로 변경할 수 있어야 했습니다. (서버에서 던져주는 값을 요리조리 가공해서)

카테고리 탭에 따라 하나의 공통 컴포넌트 내에서 탭 별로 다른 컨텐츠를 보여줘야 했기 때문에, 공통 컴포넌트 내에 각각의 카테고리 탭에서 props로 받아온 api를 한 번 더 호출하여 컨텐츠들을 동적으로 보여주었습니다.

데이터 흐름

1️⃣ 홈 화면 진입 후 카테고리 탭 데이터와 하위 컨텐츠들의 api가 담긴 리스트를 받음

2️⃣ 공통 컴포넌트에서 컨텐츠들의 api 호출하여 비트나 플레이 등의 데이터 리스트를 저장

3️⃣ 저장된 데이터에 있는 responseType 을 가지고 UI 컴포넌트를 나눠 화면에 보여줌

그래서 여러 컨텐츠의 api 호출을 위해 사용한 것이 Promise함수입니다. 사실 요새는 async-await 구문을 이용하여 더욱 간단하게 처리하는 경우가 많습니다. 그러나 async-await구문은 배열을 인자로 받을 수는 없기 때문에, 배열 비동기 처리는 promise.all로 해결하였습니다.

한번에 비동기 작업을 실행하고 결과 취합하기 (Promise.all)

Promise.all 을 통한 비동기 반복문 처리는 모든 비동기 작업을 동시에 수행합니다. 예를 들어 10개의 비동기 작업을 Promise.all을 통해 처리할 경우, 자바스크립트는 10개의 비동기 작업을 동시에 시작한 후, 10개의 작업이 모두 끝나는 순간 결과를 취합해 배열로 만듭니다.

/**
   * 컨텐츠 별로 api 호출하여 result 값 저장
   */
const getApiList = useCallback(
    async (list) => {
      if (!list) return;

      const newList = [...list];

      await Promise.all(
        newList.map((item) =>
          getHomeContents(item.apiUrl).then((response) => (item.result = response)),
        ),
      );
      setContentList(newList);

   ... 중략

탭에 처음 진입할 때, 서버에서 받은 api 배열을 map 메서드로 돌리면서 새로운 배열(newList)에 promise의 result를 담았습니다.

UI 컴포넌트 생성

reponseType 값을 활용하여 UI 컴포넌트를 분리하였습니다. 아까 컴포넌트 리팩토링에서 말했던 UI 기능 별로 나눴던 컴포넌트들을 호출하는 함수를 만들어 화면에 그려주었습니다.

/**
   * contentList에서 responseType으로 분기하여 보여주기
   */
  const getContentItem = (content) => {
    switch (content.responseType) {

      // 정방형 large 리스트
      case BEAT_LIST_LARGE :
        return (
           <HorizontalLargeList title={content.name} subTitle={content.description} data={content.result.articles} navigation={navigation} onPressMore={onPressMore}/>
        );

      // 정방형 small 리스트 - 새로운 비트
      case BEAT_LIST_SMALL:
        return (
          <> 
... 중략

🚨 리뷰 후에 발견한 문제점

Promise.all 의 문제

홈 비동기 작업에서 Promise.all를 이용해 로직을 실행하였지만 Promise.all 사용의 문제점을 발견하였습니다.

1️⃣ 배열로 받은 promise 중 단 하나의 요소만 에러를 낸다면 전체 Promise.all의 결과가 에러로 처리된다는 것(처음으로 reject 된 프로미스의 reason을 가지고 reject 됨)

2️⃣ 그렇다고 해서 나머지 promise들이 취소되지는 않는다는 것

3️⃣ 병렬로 호출을 진행하는 듯 하지만 순서에 상관없이 result 값을 내보낸다는 사실

이 후 팀장님과의 회의 후에 다시 로직을 짜서 리팩토링을 진행하기로 하였습니다😭


✅ 문제점 수정 (22.12.20)

Promise.allSetteld를 사용하면 되는 간단한 문제라고 생각했지만, React Native에서의 또다른 문제가 있었습니다. React Native 에서 Promise.allSetteld 를 지원하지 않아 undefined가 된다는 점입니다.

https://github.com/facebook/react-native/issues/30236

facebook/react-native github Issue 탭에도 있지만 따로 지원한다는 내용은 없어, Promise.allSetteld의 역할을 실행할 수 있는 promiseAllSettled 함수를 만들어 해결하였습니다.

// React Native에서 Promise.allSetteld 대신 사용할 수 있는 함수
const promiseAllSettled = (promises) => {
    return Promise.all(
      promises.map((promise) => promise.then((value) => ({ state: 'fulfilled', value })).catch((reason) => ({ state: 'rejected', reason }))),
    );
  };

함수를 사용한 후 받는 response값을 확인해보니, state(’fulfilled’ or ‘rejected’)와 value로 이루어진 객체 배열을 받을 수 있었습니다. state값을 통해 성공 여부를 확인하고 로직을 이어가게 되니, 통신 실패로 인한 위험부담을 줄일 수 있었습니다.


최종 화면


마치며

지금까지 홈 개편 과정을 소개드렸습니다. 앱 화면 자체를 바꾸는 프로젝트 진행해 본 적이 없어서 시행착오가 굉장히 많았습니다. 설계 시에 문제점을 먼저 파악했더라면 하는 아쉬움도 남았지만 완성하여 배포, 서비스되고 있는 걸 보니 굉장히 뿌듯하였습니다. 그리고 가장 놀라웠던 경험은 실사용자에게 바뀐 UI가 앱을 사용하고 훨씬 편하다는 피드백을 받았을 때 였습니다. 제 모토이기도 하지만 개발자로서 사용자에게 좋은 경험을 만들어준다는 것이 얼마나 중요한 지 다시 한 번 알게 되는 작업이었습니다!

profile
RN App Developer

0개의 댓글