React Native

[React Native] 스크린 터치 시 컴포넌트 hide/show 구현

Woonys 2022. 2. 13. 03:08
반응형

어쩌다 보니 졸지에 백인데 프론트를 맡아서 하고 있는 처지이나..예전에 디자인도 공부했고 보여지는 것에 관심이 많은지라 프론트도 잘할 수 있을 것이라 생각해 호기롭게 맡았다.

 

결과는 처참 그 자체..인 것 같았으나! 오늘 드디어 어찌저찌 문제를 해결했다. 엄밀히 말하면 반쪽짜리로 구현했지만, 일단 1차적으로 글을 쓰고 다시 고민해 최종적으로 결과물을 구현해볼 예정이다.(수정 - 문제 해결 완료! 하단에 추가)

 

현재 우리 팀의 아이템은 <On Air Super Live: 다시 보고 동시에 보는 라이브 커머스 플랫폼>이다. 여기서 내가 맡은 부분은 스트리머/뷰어가 라이브 방송을 켰을 때의 화면을 구현하는 것을 맡았다. 관련해서 스켈레톤 코드는 github에서 스트리밍 오픈소스를 참고했다. 참조한 소스코드가 클래스형 컴포넌트로 구성되어 있어 본 코드는 클래스형으로 구현했음을 미리 적는다.

 

여기서 추가해야 할 주요 기능 중 하나는 <스트리밍 화면을 터치 시 채팅을 비롯한 관련 정보가 보이지 않게 하는 것>이다. 이는 오픈소스에 미처 구현되어있지 않은 기능이었다.

 

그립 라이브 스트리밍 이미지 참조

 

이것만 보면 엄청 간단해보이는데.. 문제는 내가 리액트도 처음인데 리액트 네이티브도 처음이라는 것..

 

이것이 만약 웹이면 각 태그에 <class name>을 붙여서 다른 태그를 제어할 수 있을 것이다. 하지만 리액트 네이티브에서는 HTML 태그를 쓰는 방식이 아닌 <View> 태그를 쓰는데다가 각 View 태그에 class name을 달 수 없다. 그렇기에 자식 컴포넌트가 아닌 병렬적으로 되어 있는 다른 태그에 대한 제어가 불가능하다. 결과적으로 나 역시 당장은 하위 컴포넌트로 넣어서 해결하긴 했다.

 

좀 더 디테일하게 현재 상황을 설명해보겠다. 대략적인 문제 설명은 아래와 같다.

현재 header, center, footer에 대한 각 <View> 태그는 병렬적으로 나열되어 있는 상황이다.

<View style={styles.header} />
<View style={styles.center} />
<View style={styles.footer}>
    {this.renderChatGroup()}
    {this.renderListMessages()}
</View>

여기서 header, center, footer의 style은 아래와 같이 flex로 구분해놨다. 이런 상황에서 center를 터치하면 footer에 있는 

{this.renderChatGroup()}, {this.renderListMessages()} 가 사라져야 한다.

 

1. constructor에 state(isVisibleFooter) 추가

export default class Streamer extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          isVisibleFooter: true,
        };
    }

특정 <View>가 보이는 상태인지 아닌지를 표시하기 위해 state에 bool type을 추가한다. isVisibleFooter가 true이면 footer view를 보이게, false이면 footer view를 가리게 할 것이다.

 

2. 관련 메소드(renderChatGroup())에 if문 추가

컴포넌트 내 메소드 중, footer에 들어가는 컴포넌트를 렌더링해주는 메소드는 renderChatGroup()이다. 아래 이미지와 같이 글자를 입력하는 부분, 메시지를 send하는 버튼, 하트를 날리는 버튼 총 세 가지로 구성되어 있다.

원래 코드는 아래와 달리 const { isVisibleFooter }가 빠져있고 if문이 없이 곧바로 return을 선언하는 구조로 되어있었다. 여기서 isVisibleFooter를 추가하고 if문을 추가해 isVisibleFooter가 true일 때 렌더링을 리턴하는 식으로 변경한다.

 

renderChatGroup = () => {
    const { isVisibleFooter } = this.state;
    if (isVisibleFooter) {
      return (
        <ChatInputGroup
          onPressHeart={this.onPressHeart}
          onPressSend={this.onPressSend}
          onFocus={this.onFocusChatGroup}
          onEndEditing={this.onEndEditing}
        />
      );
    }
    return null;
  };

 

3. 함수 작동시 isVisibleFooter의 state를 변경하는 메소드(onPressVisible())를 생성한다.

함수가 동작하면 isVisibleFooter의 state를 반대로 변경하는 메소드를 추가한다.

참조한 소스 코드가 클래스형 컴포넌트인데 워낙 코드 길이가 긴데다 이것저것 많이 얽혀있어 함수형 컴포넌트로 리팩토링할 엄두가 나지 않았다. 고로 setState()를 이용했다.

클릭은 아래 render()에서 'onPress={onPressVisible}'을 추가하면 된다.

  onPressVisible = () => {
    // 작동한다.
    const { isVisibleFooter } = this.state;
    this.setState(() => ({ isVisibleFooter: !isVisibleFooter }));
    console.log(isVisibleFooter); // 작동한다.
  };

 

4. render(): <TouchableWithoutFeedback> 컴포넌트에 onPress={onPressVisible}를 추가한다.(아직 미해결)

 

아직 덜 해결한 부분. 일단 기본 골자는 화면 터치의 경우 그에 대한 액션을 줄 필요가 없으니 TouchableWithoutFeedback 컴포넌트로 만든다. 이걸로 center View 컴포넌트를 감싼다.

 

문제는, 위에서 말했던 것처럼 center View 컴포넌트와 footer view를 병렬로 놓고 싶었는데 그렇게 하면 center에서 state를 변경해도(setState()) center 컴포넌트 및 그 하위 컴포넌트만 리렌더링이 되지, 이와 상관없는 footer 컴포넌트가 리렌더링되지 않는다.

 

그렇다고 center 컴포넌트와 footer 컴포넌트를 둘다 한 번에 TouchableWithoutFeedback 컴포넌트로 묶으면 시뻘건 에러 창을 보게 된다. TouchableWithoutFeedback 컴포넌트 특성 상 하나의 자식 컴포넌트만 가지게 되어 있기 때문이다.

 

그러면 어찌하나..이번에는 <View> 태그로 center와 footer를 묶어봤는데 이번에는 터치 자체가 작동하지 않는다. <View> 태그에서는 터치가 먹지 않는듯.

 

찾다찾다 <Fragment> 컴포넌트를 이용하면 두 컴포넌트를 묶을 수 있다는데 이러면 터치는 커녕 컴포넌트 자체가 화면에 보이지 않는다.

 

궁여지책으로 아래 코드와 같이 center 컴포넌트에 footer를 하위 컴포넌트로 넣는 방식을 쓰니 작동은 한다. 문제는 footer가 flex로 되어 있어서 맨 밑에 가 있어야 할 애가 맨 위로 가버렸다는 문제가 발생하지만..내일 고쳐보기로..

 

render() {
    return (
      <SafeAreaView style={styles.container}>
        (...)
        <SafeAreaView style={styles.contentWrapper}>
          <View style={styles.header} />
          <TouchableWithoutFeedback onPress={this.onPressVisible}>
            <View style={styles.center}>
              <View style={styles.footer}>
                {this.renderChatGroup()}
                {this.renderListMessages()}
              </View>
            </View>
          </TouchableWithoutFeedback>
        </SafeAreaView>
      </SafeAreaView>
    );
  }
}

 

전체 코드는 아래와 같다. (본 기능을 구현하는데 필요한 부분만 넣음)

export default class Streamer extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          isVisibleFooter: true,
        };
    }
    
    renderChatGroup = () => {
    const { isVisibleFooter } = this.state;
    if (isVisibleFooter) {
      return (
        <ChatInputGroup
          onPressHeart={this.onPressHeart}
          onPressSend={this.onPressSend}
          onFocus={this.onFocusChatGroup}
          onEndEditing={this.onEndEditing}
        />
      );
    }
    return null;
  };
  
  onPressVisible = () => {
    const { isVisibleFooter } = this.state;
    this.setState(() => ({ isVisibleFooter: !isVisibleFooter }));
  };

  render() {
      return (
        <SafeAreaView style={styles.container}>
          <SafeAreaView style={styles.contentWrapper}>
            <View style={styles.header} />
            <TouchableWithoutFeedback onPress={this.onPressVisible}>
              <View style={styles.center}>
                <View style={styles.footer}>
                  {this.renderChatGroup()}
                  {this.renderListMessages()}
                </View>
              </View>
            </TouchableWithoutFeedback>
          </SafeAreaView>
        </SafeAreaView>
      );
    }
  }

 

 

*수정* 기능 구현 완료(22/02/13)

 

5. header/center/footer -> header/body 구조로 변경 & body 내에 자식 컴포넌트로 footerBar 삽입 & footerBar 스타일 변경

 

간단한 문제였는데..괜히 이상한 데서 똥고집을 부렸네..

 

내가 구현하고 싶었던 방식은 header/center/footer가 병렬적으로 나열되어 있는 상황에서 center를 클릭 시 footer의 요소가 변경되도록 하는 것이었는데, 그냥 center/footer를 자식 관계로 만들면 그만이었다. 굳이 병렬로 할 필요가 없었다.

 

여기서 무엇을 걱정했냐, 그러면 스타일을 어떻게 변경해야 할지 감이 오지 않았던 게 큰 문제였다. 기존 스타일을 보자.

 

const styles = StyleSheet.create({
  
(...)
  header: { flex: 0.1, justifyContent: 'space-around', flexDirection: 'row' },
  footer: { flex: 0.1 },
  center: { flex: 0.8 },

 

이는 위에서 그렸던 그림처럼 병렬로 배치된 레이아웃이다. 계속 이 틀 안에서 사고하다보니 "center가 0.8이고 footer가 0.1이면 center 안에 footer를 넣을 때 위치 조정을 어떻게 해야 하지?"가 의문이 풀리지 않았던 것.

 

이는 flex에 대해 이해하면 간단히 해결되는 문제였다.

 

flexbox는 상대적인 비율로 위치를 조정하는 알고리즘이다. 위처럼 전체 비율을 1이라고 하면 헤더에 0.1, 센터에 0.8, 푸터에 0.1을 주면 어떤 폰이건 간에 상대적인 비율로 위치를 조정하는 것. (flexbox에 대한 react-native document)

 

그렇다면, center와 footer를 하나로 나타내는 body(0.1+0.8=0.9)를 만들고 그 안에 footerBar를 넣어주면 되지 않을까?

 

그러면 여기서 또 문제. footerBar를 어떻게 밑으로 가게 하나? 그냥 넣으면 리액트 네이티브에서는 무조건 위에서부터 정렬하는 방식이라 아래로 내려가지 않는다.

 

처음에는 이를 해결하기 위해 절대적으로 위치를 조정하는 방식을 적용해봤다. height를 내리는 방식이었는데, 이렇게 하면 위치가 하단으로 내려가긴 한다.

 

footerBar: { height: 600 },

 

 

 

하지만 모든 폰에서 이것이 정확히 위의 이미지처럼 위치한다는 보장이 없기 때문에 (위에서부터 600px 내린 것이니 폰이 길면 하단이 아니라 중간에 위치할 것이고 폰이 작으면 아예 저 밑으로 내려가버릴 것이다.) 이와 같은 방식은 좋지 않다.

 

flex의 방향을 분명히 뒤집을 수 있을 것 같은데...하며 열심히 뒤적거린 결과 찾아냈다! 바로 flexDirection: 'row-reverse'를 이용하는 것.

 

https://github.com/facebook/react-native/commit/d43e0db81e86d4d03638cd17034086717fe715a3

 

Add support for reverse flex directions on Android and iOS · facebook/react-native@d43e0db

Summary: This PR adds support for both 'row-reverse' and 'column-reverse' for Android and iOS and is based on the changes in #6683 that looked like it's all but abandoned. It al...

github.com

 

위에서 설명한 것처럼, flex의 direction은 기본적으로 위->아래로 내려오는 방식이다. 그런데 row-reverse를 추가하면 반대 방향으로 잘 적용되는 것을 알 수 있다. (justifyContents 역시 화면 배치 관련 기능인데, 나중에 찾아보면 좋을듯)

 

footerBar: { flex: 1, justifyContent: 'space-around', flexDirection: 'row-reverse' },

 

여기서 주의! flex 비율이 1로 되어있는 게 보일텐데, 만약 아래에 위치시켜야겠다!고 생각해서 처음 footer 쓸 때와 같이 flex를 0.1로 해버리면 안된다. 이 flex 자체는 화면 비율을 위->아래로 하는 방식이기 때문. 말로 하면 잘 전달이 안되니 그림으로 보자.

 

flex 비율에 따라 화면 중 얼마만큼을 쓸지가 정해지는 방식이다. row-reverse를 써서 화면 맨 하단에 위치시키고 싶다면 위와 같지 전체 화면에 대해 방향을 뒤집어야 하지, 예를 들어 flex=0.5로 해버리면 절반만큼의 화면에서 하단으로 위치시키는 방식이 되어버린다.

 

어쨌건 간에 결과적으로 잘 해결했다! 아자자!

반응형