[프로젝트] RN 프로젝트 마크업 마무리 회고

Jade·2023년 5월 5일
4

프로젝트

목록 보기
26/28

사이드 프로젝트 진행이 느리지만 꾸준하게 진행되고 있는데, 기록을 전혀 하지 못해서 휴일인 김에 게시글로 남겨놓고 가려고 작성한다.


🟢 내가 작성했던 초기 와이어프레임

🟢 디자인 최종

🟢 유저플로 최종


🤔 React Native의 특징

  • React Native는 많은 것들이 컴포넌트로 이미 만들어져 있다. 예를 들어 React를 사용할 때 div 태그로 원한다면 다양한 요소들을 만들 수 있었지만, React Native는 버튼이 필요하다면 Button 컴포넌트나 Touchable, Pressable 등 이미 작성된 컴포넌트를 사용해야 한다. 또 div와 비슷한 기능을 하는 View 컴포넌트 내에 텍스트를 입력하기 위해서는 Text 컴포넌트를 사용해야만 텍스트를 입력할 수 있다.

  • 이런 점이 편리하다고 생각할 수도 있지만 개인적으론 한계가 지어진 느낌이 강해서 초반에는 어색하게 느껴졌다. 게다가 처음 React Native를 사용했을 때 컴포넌트 종류를 잘 알고 있지 못했기 때문에 용도에 맞는 컴포넌트를 찾아서 사용해야 한다는 점이 조금 귀찮게 느껴진 것도 사실이다. 하지만 이렇게 요소들의 용도가 확정지어져 있기 때문에 div로 가득한 코드보다 가독성면에서 좀 더 좋을 수 있다는 장점이 있을 것 같다는 생각도 동시에 들었다. (컴포넌트 이름들이 꽤 직관적임 ex: View, Text, Touchable, FlatList...etc)


🤔 React Native의 이벤트 객체

  • 이전에 리액트로 탭 형식의 UI를 만들 때는 각 탭이나 버튼에 name 속성을 달아주어서 그 name 속성을 통해 어느 탭이나 버튼이 눌렸는지에 대해 구분했었다. 이벤트 객체 안에 해당 내용이 담겨 있었기 때문에 e.target.name과 같은 형태로 해당 속성을 꺼내볼 수 있었다. 근데 RN에서 이벤트 객체를 콘솔에 찍어봤더니 생전 처음보는 녀석들이 있는 걸 발견했다... 사실 이벤트 객체라는 이름도 다르다 nativeEvent라고 하더라...
{
    changedTouches: [PressEvent],
    identifier: 1,
    locationX: 8,
    locationY: 4.5,
    pageX: 24,
    pageY: 49.5,
    target: 1127,
    timestamp: 85131876.58868201,
    touches: []
}
  • changedTouches : Array of all PressEvents that have changed since the last event.
  • identifier : Unique numeric identifier assigned to the event.
  • locationX: Touch origin X coordinate inside touchable area (relative to the element).
  • locationY : Touch origin Y coordinate inside touchable area (relative to the element).
  • pageX : Touch origin X coordinate on the screen (relative to the root view).
  • pageY : Touch origin Y coordinate on the screen (relative to the root view).
  • target : The node id of the element receiving the PressEvent.
  • timestamp : Timestamp value when a PressEvent occurred. Value is represented in milliseconds.
  • touches : Array of all current PressEvents on the screen.

🤔 React Native에서 말줄임표 적용하기

Text 컴포넌트에서 numberOfLines 속성을 사용하면 Text 컴포넌트가 먹을 행을 설정할 수 있다.
+@ Text나 그 상위 컴포넌트에 크기 관련 설정 (flex)이 있어야 가능함.

<View style={styles.songInfo}>
	<Text style={styles.title} numberOfLines={1}>
          {title}
	</Text>
...
</View>

위 View 컴포넌트에 CSS가 flex: 1 이 적용되어 있으므로
하위에 있는 Text 컴포넌트 내 title이라는 텍스트는 한 줄이 넘어가게 되면 말줄임표로 나타나게 된다.


🤔 이벤트 전파 막기

모달 컴포넌트 작성하면서 백드롭 부분을 눌렀을 때 닫히게 하기 위해 백드롭 자체를 TouchableOpacity로 설정했었다.

그랬더니 내부에 있는 모달 컨테이너를 눌렀을 때도 닫히길래... 단순히 TouchableOpacity 내에서 e.stopPropagation을 사용하려고 했는데 안 먹히더라...

stackOverFlow에서 발견한 해결법은 백드롭인 TouchableOpacity 내부에 있는 View(모달 컨테이너)에 onStartShouldSetResponderonTouchEnd props를 사용하는 것이었다.

RN의 View 컴포넌트 문서에 onStartShouldSetResponder 부분을 보면 Does this view want to become responder on the start of a touch?라고 적혀있는 걸 볼 수 있다.

View도 터치의 응답자(Gesture Responder System: 앱 내에서 일어나는 제스처들의 라이프사이클을 관리해주는 시스템으로, 스크롤링, 슬라이딩, 탭, 터치 등의 사용자 간에 일어나는 상호작용에 대해 응답자가 될 것인지 관리하는 시스템)가 될 수 있는데, 응답자가 되기 위해 설정할 수 있는 props 중 하나가 onStartShouldSetResponder이다.

해당 뷰 컴포넌트가 터치가 시작될 때 응답자가 되기를 원하냐는 건데, 이걸 true로 설정해준다. 뷰가 이벤트를 잡아서 멈추게 하기 위해서 일단 responder가 되도록 하는 것 같다.
(해당 props를 빼고 아래 onTouchEnd만 사용했을 때는 이벤트 전파가 막히지 않았음)

그리고 나서 onTouchEnd (터치 스크린에서 요소에서 손가락을 떼면 이벤트 발생) 부분에 e.stopPropagation을 걸어주면 잡은 이벤트를 퍼져나가지 않게 할 수 있다.

//backdrop은 GlobalModal에 포함되어 있고, 
//GlobalModal 내에서 ModalState에 따라 각 모달들이 렌더링 된다

//GlobalModal 파일 

const GlobalModal = () => {
  const { isOpen, modalType, props } = useRecoilValue(ModalState);
  const reset = useResetRecoilState(ModalState);

  if (!isOpen) return;

  const modal = {
    addSong: <AddSongModal {...props} />,
    confirm: <ConfirmModal {...props} />,
    delete: <DeleteModal {...props} />,
    editPlaylist: <EditPlaylistModal {...props} />,
    addPlaylist: <AddPlaylistModal {...props} />,
  };

  return (
    <Modal animationType="fade" transparent={isOpen} visible={isOpen}>
      <TouchableOpacity
        activeOpacity={1}
        style={styles.backdrop}
        onPress={() => {
          reset();
        }}
      >
        {modal[modalType]} // 각 모달들이 렌더 되는 위치 
      </TouchableOpacity>
    </Modal>
  );
};

export default GlobalModal;


________

//ConfirmModal 파일 

const ConfirmModal = ({ title, subTitle, buttonText, handler }) => {
  const reset = useResetRecoilState(ModalState);

  return (
    //가장 바깥쪽 View에 걸어줌 
    <View
      style={styles.modalContainer}
      onStartShouldSetResponder={(event) => true}
      onTouchEnd={(e) => {
        e.stopPropagation();
      }}
    >
      <Text style={styles.title}>{title}</Text>
      <Text style={styles.subTitle}>{subTitle}</Text>
      <View style={styles.buttonBox}>
        <View style={{ flex: 1, marginRight: 8, height: 40 }}>
          <RowButton
            text={buttonText}
            color="lime"
            buttonHandler={() => handler()}
          />
        </View>
        <View style={{ flex: 1, height: 40 }}>
          <RowButton
            text="취소"
            color="gray"
            buttonHandler={() => {
              reset();
            }}
          />
        </View>
      </View>
    </View>
  );
};

export default ConfirmModal;

참고
1. https://ko.legacy.reactjs.org/docs/events.html
2. https://velog.io/@coffeemj/Gesture-Responder-System
3. https://stackoverflow.com/questions/31866671/how-to-stop-touch-event-propagation-in-react-native


🤔 모달 적용시 만난 hook 에러 해결기

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem., js engine: hermes

프론트 팀원분이 어제 내가 리팩토링 해둔 모달을 사용하니 해당 에러가 떴다고 해서 직접 해결해보고자 했다
에러에서 던져준 힌트를 보았을 때 1번이나 3번은 npm ls React 명령어를 사용해 확인해보았을 때 별 문제가 없는 것으로 보여져 'hooks 는 function component 내부에서만 선언 가능하다'는 2번째 부분이 문제가 아닐까 해서 중점적으로 살펴보았다.

1. 자기 자신을 포함한 모달을 불러오는 컴포넌트

이 에러가 MusicItem 컴포넌트에서만 발생했기 때문에 해당 컴포넌트의 문제가 뭔지 살펴봤다.
일단 처음으로 발견한 문제는 Search 페이지에서 MusicItem이 React Native의 FlatList 컴포넌트를 통해서 리스트로 렌더링 되고 있었는데, 이 MusicItem을 클릭했을 때 뜨게 되는 AddSongModal 내부에 MusicItem이 존재한다는 것이었다...

일단 모달 내부에 있는 노래 정보 부분을 MusicItem 컴포넌트가 아니도록 변경하기로 했다.

2. FlatList가 렌더하는 아이템 내부에는 훅을 사용할 수 없다

1번을 변경하고 나서도 동일 에러가 계속해서 떴고, 이 에러는 AddSongModal을 다른 페이지에서 불러올 때는 뜨지 않았다.

그래서 Search 페이지에 문제가 있는 걸까, 생각해보았고 특히 MusicItem이 직접적으로 연관되는 FlatList가 문제인가? 하는 생각이 들어 검색을 해보게 된다.

stackOverFlow에서 발견한 이 글에 따르면 아래와 같이 코드를 고칠 수 있을 것 같았다.


(윗 부분 코드 생략)
<FlatList
        style={styles.resultList}
        data={searchDummy.data}
        renderItem={({ item }) => (
          <MusicItem item={item} onPress={() => setModal(songModal)} />
        )}
        contentContainerStyle={{ rowGap: 8 }}
      />

원래는 MusicItem 컴포넌트 내부에서 가장 바깥쪽인 TouchableOpacity의 onPress 속성에 recoil ModalState를 이용해 모달을 띄워주고 있었는데, 이 과정을 FlatList가 있는 Search 페이지에서 해보기로 했다.

2.5 onPress는 MusicItem에서 해주는 게 맞았다 !

위 StackOverFlow 답변을 다시 찬찬히 읽어보는데 FlatList의 renderItem에 전달해주는 요소를 함수 안으로 바꿔준 거지 실제로 훅을 FlatList가 있는 페이지에서 실행하라는 말이 아닌 것 같아서 아래와 같이 코드를 변경했다.

 <FlatList
        style={styles.resultList}
        data={searchDummy.data}
        renderItem={({ item }) => <MusicItem item={item} />}
        contentContainerStyle={{ rowGap: 8 }}
      />
const MusicItem = ({ item }) => {
  const { num, singer, title } = item;
  const setModal = useSetRecoilState(ModalState);

  const songModal = {
    isOpen: true,
    modalType: 'addSong',
    props: {
      selectedSong: {
        title,
        singer,
        num,
      },
      handler: () => {
        console.log('addSong...!');
      },
    },
  };

return (
 <TouchableOpacity
      style={styles.container}
      onPress={() => {
        setModal(songModal);
      }}
    >
      (중략)

</TouchableOpacity>

  );
};

export default MusicItem;

짠 !!!!!! 해결 !!!!!!!!!!

결국 MusicItem을 그냥 넘기게 되면 컴포넌트라는 것을 인지하지 못하고, 왜 이상한 함수에서 hook을 사용했느냐!!!! 라고 하면서 에러를 띄우게 되는 것 같다.
그걸 방지하기 위해서 MusicItem을 컴포넌트 형태로 화살표 함수 안에 넣어서 FlatList에 전달해서 MusicItem은 컴포넌트라 안에서 훅을 사용할 수 있다고 알려주는듯.


🤔 React Native에서 Recoil로 모달 상태 관리

사실 이전 과외차이 프로젝트를 할 때와 크게 다를 것 없이 설정했다.

다른 점이 있다면 이전에는 글로벌 모달에 백드롭만 담아줬었는데, React Native에서는 모달을 사용하게 위해서 Modal 컴포넌트를 항상 모달의 가장 바깥에 감싸줘야했기 때문에 글로벌 모달의 가장 바깥이 Modal 컴포넌트가 되고, 그 내부에 백드롭이 들어가도록 만들었다.

import { atom } from 'recoil';

const ModalState = atom({
  key: 'ModalState',
  default: {
    isOpen: false,
    modalType: '',
    props: {},
  },
});

export default ModalState;
//GlobalModal.js
import ModalState from '../../recoil/modal.js';
import { useResetRecoilState, useRecoilValue } from 'recoil';
import { Modal, TouchableOpacity } from 'react-native';
import {
  AddSongModal,
  ConfirmModal,
  DeleteModal,
  EditPlaylistModal,
  AddPlaylistModal,
} from './index.js';
import styles from './GlobalModal.style';

const GlobalModal = () => {
  const { isOpen, modalType, props } = useRecoilValue(ModalState);
  const reset = useResetRecoilState(ModalState);

  if (!isOpen) return;

  const modal = {
    addSong: <AddSongModal {...props} />,
    confirm: <ConfirmModal {...props} />,
    delete: <DeleteModal {...props} />,
    editPlaylist: <EditPlaylistModal {...props} />,
    addPlaylist: <AddPlaylistModal {...props} />,
  };

  return (
    <Modal animationType="fade" transparent={isOpen} visible={isOpen}>
      <TouchableOpacity
        activeOpacity={1}
        style={styles.backdrop}
        onPress={() => {
          reset();
        }}
      >
        {modal[modalType]}
      </TouchableOpacity>
    </Modal>
  );
};

export default GlobalModal;

다만 좀 유의할 부분이 있다면, 사실 리코일 자체가 React Native에 완전히 호환된다고 말하기는 어렵다는 것이다. 일단 우리 프로젝트에서는 간단하게 모달 상태 정도만 관리할 거라 사용하긴 했지만 공식적으로 React Native를 지원하지 않는다는 게 Recoil의 답변인듯.

React Native (Experimental)
Recoil should now work with the React Native environment. However, similar to server side rendering, it is not officially supported.
https://recoiljs.org/ko/blog/2020/10/30/0.1.1-released/

profile
키보드로 그려내는 일

0개의 댓글