리액트 네이티브 튜토리얼
[#1 리액트 네이티브 맛보기]
[#2 Component와 메인화면 분할]
[#3 레이아웃 구성하기]
[#4 레이아웃 구현 마무리하기]
[#5 state]
[#6 설정 Modal 만들기]
[#7 설정 Modal 기능 추가하기]
[#8(구현 끝) 채팅기능 구현하기]

 

이번 글에서는 설정 모달의 기능들을 구현해보겠습니다.

가장 먼저 모달 외부를 눌렀을 때, 모달창이 닫히게 해보겠습니다.

//App.js

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      dday: new Date(),
      ddayTitle: '테스트 디데이',
      chatLog: [],
      settingModal: false,
    }
  }

App.js로 잠시 돌아가서, state에 모달창이 닫혀있는지 열려있는지를 판단하는 settingModal이라는 state를 하나 생성해주겠습니다.

settingModal이 false면 모달창이 표시되지 않고 true면 모달창이 표시되게끔 하는거에요.

//App.js

..생략
        <View style={styles.chatView}>
          <ScrollView style={styles.chatScrollView}>
          </ScrollView>
          <View style={styles.chatControl}>
            <TextInput style={styles.chatInput}/>
            <TouchableOpacity style={styles.sendButton}>
              <Text>
                전송
              </Text>
            </TouchableOpacity>
          </View>
        </View>
          //이부분을 추가해주세요
          { this.state.settingModal ? <Setting/> : <></> }
        </ImageBackground>
      </View>
    );
  }

react native에서는 삼항연산자를 이용해서 컴포넌트가 조건에 따라 나타나게할 수 있습니다.

삼항연산자에대해서 잘 모르시는 분은 developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Conditional_Operator 를 읽어보시고 오시면 좋을것 같습니다.

위의 코드를 보면, this.state.settingModal이 true라면 Setting컴포넌트를 표시하고, false라면 <></>즉 빈 컴포넌트를 표시해서 true일 때만 모달을 표시할 수 있습니다.

//App.js
import React from 'react';
import { StyleSheet, Text, View, Image, TextInput,TouchableOpacity, ScrollView, ImageBackground} from 'react-native';
import Setting from './Setting.js';

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      dday: new Date(),
      ddayTitle: '',
      chatLog: [],
      settingModal: false,
    }
  }
  
  //함수 작성
  toggleSettingModal() {
    this.setState({
      settingModal: !this.state.settingModal
    })
  }
  
  ...생략

세팅 톱니바퀴 버튼을 눌렀을 때, 동작할 함수를 하나 선언해주겠습니다.

위치는 constructor바로 아래에 작성하시면 됩니다.

함수의 내용은 이전시간에 알려드린 setState를 이용해서 settingModal state를 not연산한 결과로 설정하는 것입니다.

즉 settingModal이 false였다면, 함수 실행후에는 true가 되고, true였다면 함수 실행후에는 false가 됩니다.

settingModal의 초기값은 false이므로, 톱니바퀴 버튼을 눌렀을 때 함수를 실행하면 true로 바뀌면서 모달이 표시되는 원리입니다.

//App.js

..생략

  render() {
    return (
      <View style={styles.container}>
        <ImageBackground
          style={{width: '100%', height: '100%'}}
          source={require('./images/background.png')}>

        <View style={styles.settingView}>

          <TouchableOpacity onPress={()=>this.toggleSettingModal()}> //onPress추가
            <Image source={require('./icon/setting.png')}/>
          </TouchableOpacity>
        </View>
        
        ..생략

setting의 TouchableOpacity에 onPress에 함수를 전달하면, 해당 버튼이 눌렸을 때 동작을 정의할 수 있습니다.

함수로 전달해야하기 때문에, 화살표함수를 이용해서 전달해주세요.

이제 톱니바퀴 버튼을 누르면 모달창이 열리는 것을 확인할 수 있습니다.

이때 의문이 드는점이 있을 수 있습니다.

settingModal의 state는 App.js에 정의되어있지만, 모달창의 외부를 누르는 버튼은 Setting.js에 정의되어있기 때문에, App.js에서 정의한 toggleSettingModal이라는 함수를 사용할 수 없습니다.

이때 이용하는 것이 props입니다.

Props는 읽기전용 데이터로 이를 이용해서 부모 컴포넌트의 값을 자식컴포넌트로 전달할 수 있습니다.

//App.js

..생략
        <View style={styles.chatView}>
          <ScrollView style={styles.chatScrollView}>
          </ScrollView>
          <View style={styles.chatControl}>
            <TextInput style={styles.chatInput}/>
            <TouchableOpacity style={styles.sendButton}>
              <Text>
                전송
              </Text>
            </TouchableOpacity>
          </View>
        </View>
          //이부분을 추가해주세요
          { this.state.settingModal ? 
          <Setting  modalHandler={()=>this.toggleSettingModal()} />  //modalHandler props추가
          : <></> }
        </ImageBackground>
      </View>
    );
  }

위와 같은방식으로 Setting 컴포넌트에 modalHandler라는 props를 전달하는 것입니다.

이렇게 부모컴포넌트의 toggleSettingModal이라는 함수를 modalHandler라는 props로 전달했고, Setting컴포넌트에서는 modalHandler를 호출하면 부모 컴포넌트의 toggleSettingModal함수가 실행되는 것입니다.

//Setting.js


  render() {
    return (
      <View  style={styles.container}>
      
        <TouchableOpacity 
          style={styles.background} 
          activeOpacity={1} //activeOpacity추가
          onPress={this.props.modalHandler}/> //onPress 추가
        <View style={styles.modal}>
          <Text style={styles.titleText}>설정</Text>
          ..생략

Setting.js로 돌아와서 뒷 배경을 렌더링하고 있던 TouchableOpacity에 activeOpacity={1}를 추가해줍니다.

이는 원래 기본적으로 TouchableOpacity를 눌렀을 때 깜빡이는 효과가 있는데 이를 해제해주는 옵션입니다.

그리고 onPress에 아까 Props로 전달했던 modalHandler를 설정합니다.

이미 props가 화살표함수로 전달되었기 때문에 화살표함수를 쓸 필요없이 this.props.modalHandler로 설정하면 됩니다.

이제 모달창의 바깥영역을 터치하면 모달창이 닫히게 됩니다.

다음으로는 완료버튼을 눌렀을 때, App.js의 state들이 업데이트 되도록 만들겠습니다.

//App.js
import React from 'react';
import { StyleSheet, Text, View, Image, TextInput,TouchableOpacity, ScrollView, ImageBackground} from 'react-native';
import Setting from './Setting.js';

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      dday: new Date(),
      ddayTitle: '',
      chatLog: [],
      settingModal: false,
    }
  }
  

  toggleSettingModal() {
    this.setState({
      settingModal: !this.state.settingModal
    })
  }
  
  //함수 작성
  settingHandler(title, date) {
    this.setState({
      ddayTitle: title,
      dday: date,
    });
    this.toggleSettingModal();
  }
  ...생략

다시 App.js로 돌아와서 toggleSettingModal아래에 settingHandler라는 함수를 작성해주세요.

해당함수는 인자로 title과 date를 받아서 해당 인자로 ddayTitle과 dday를 업데이트해주는 함수입니다.

또한 완료버튼을 눌렀을 때 동작할 것이기 때문에, toggleSettingModal을 호출해서 모달을 닫게끔 할게요.

//App.js

..생략
        <View style={styles.chatView}>
          <ScrollView style={styles.chatScrollView}>
          </ScrollView>
          <View style={styles.chatControl}>
            <TextInput style={styles.chatInput}/>
            <TouchableOpacity style={styles.sendButton}>
              <Text>
                전송
              </Text>
            </TouchableOpacity>
          </View>
        </View>
          {this.state.settingModal ?
            <Setting
              modalHandler={()=>this.toggleSettingModal()}
              settingHandler={(title, date)=>this.settingHandler(title, date)}/> //settingHandler추가
            : <></>}
        </ImageBackground>
      </View>
    );
  }
}

아까와 마찬가지로 settingHandler라는 props로 this.settingHandler를 전달합니다.

이번에는 함수에 title, date라는 인자가 총 두개가 필요하므로, 화살표함수로 인자를 설정해서 전달합니다.

//Setting.js

.. 생략

  render() {
    return (
      <View  style={styles.container}>
        <TouchableOpacity style={styles.background} activeOpacity={1} onPress={this.props.modalHandler}/>
        <View style={styles.modal}>
          <Text style={styles.titleText}>설정</Text>
          <TextInput
            style={styles.ddayInput}
            value={this.state.title}
            onChangeText={(changedText)=>{this.setState({title: changedText})}}
            placeholder={"디데이 제목을 입력해주세요."}/>
          <DatePicker
            date={this.state.date}
            mode="date"/>
            //onPress 추가
          <TouchableOpacity onPress={()=>this.props.settingHandler(this.state.title, this.state.date)}>
            <Text style={styles.doneText}>
              완료
            </Text>
          </TouchableOpacity>
        </View>
      </View>
    );
  }
}

완료를 감싸고 있는 TouchableOpacity의 onPress를 설정해주세요.

이제 완료를 누르게 되면, App.js의 title과 date가 세팅에서 설정된 값으로 바뀌면서, title렌더링도 바뀌게 됩니다.

이제 남은 디데이 일자와 날짜를 반환하는 함수를 작성하겠습니다.

//App.js
import React from 'react';
import { StyleSheet, Text, View, Image, TextInput,TouchableOpacity, ScrollView, ImageBackground} from 'react-native';
import Setting from './Setting.js';

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      dday: new Date(),
      ddayTitle: '',
      chatLog: [],
      settingModal: false,
    }
  }
  

  toggleSettingModal() {
    this.setState({
      settingModal: !this.state.settingModal
    })
  }
  
  settingHandler(title, date) {
    this.setState({
      ddayTitle: title,
      dday: date,
    });
    this.toggleSettingModal();
  }

  //함수 작성
  makeDateString() {
    return this.state.dday.getFullYear() + '년 ' + (this.state.dday.getMonth()+1) + '월 ' + this.state.dday.getDate() + '일';
  }
  //함수 작성
  makeRemainString() {
    const distance = new Date().getTime() - this.state.dday.getTime();
    console.log(new Date(), this.state.dday,distance / (1000 * 60 * 60 * 24) )
    const remain = Math.floor(distance / (1000 * 60 * 60 * 24));
    if(remain < 0) {
      return 'D'+remain;
    } else if (remain > 0) {
      return 'D+'+remain;
    } else if (remain === 0) {
      return 'D-day';
    }
  }
  ...생략

this.state.dday를 이용해서 00년 00월 00일 을 반환하는 makeDateString을 선언해주세요.

this.state.dday를 이용해서 오늘 날짜와 비교해서 D-00또는 D+00또는 D-day를 반환하는 makeRemainString을 선언해주세요.

//App.js

 render() {
    return (
      <View style={styles.container}>
        <ImageBackground
          style={{width: '100%', height: '100%'}}
          source={require('./images/background.png')}>

        <View style={styles.settingView}>

          <TouchableOpacity onPress={()=>this.toggleSettingModal()}>
            <Image source={require('./icon/setting.png')}/>
          </TouchableOpacity>
        </View>
        <View style={styles.ddayView}>
          <Text style={styles.titleText}>
            {this.state.ddayTitle}까지
          </Text>
          <Text style={styles.ddayText}>
            {this.makeRemainString()}
          </Text>
          <Text style={styles.dateText}>
            {this.makeDateString()}
          </Text>
        </View>
        ..생략

App.js의 dday렌더링부분에 괄호를 이용해서 함수의 반환값을 렌더링하게 해주세요.

이렇게 작성하면, state가 업데이트 될 때마다 {}안의 함수들이 실행되어 변화된 반환값을 렌더링하게 됩니다.

이전에 사용했던 datepicker의 설정을 완료하지 않아서 date를 선택했을 때 state를 변경하도록 설정해주겠습니다.

//Setting.js

 render() {
    return (
      <View  style={styles.container}>
        <TouchableOpacity style={styles.background} activeOpacity={1} onPress={this.props.modalHandler}/>
        <View style={styles.modal}>
          <Text style={styles.titleText}>설정</Text>
          <TextInput
            style={styles.ddayInput}
            value={this.state.title}
            onChangeText={(changedText)=>{this.setState({title: changedText})}}
            placeholder={"디데이 제목을 입력해주세요."}/>
            
            //onDateChange 추가
          <DatePicker
            date={this.state.date}
            onDateChange={(date)=>{this.setState({date: date})}}
            mode="date"/>
          <TouchableOpacity onPress={()=>this.props.settingHandler(this.state.title, this.state.date)}>
            <Text style={styles.doneText}>
              완료
            </Text>
          </TouchableOpacity>
        </View>
      </View>
    );
  }
}

onDateChange는 DatePicker에서 지원하는 date가 선택될 때마다 수행되는 함수입니다.

TextInput의 onChangeText와 비슷한 역할이에요.

지금까지 진행상황을 보면, 설정한 타이틀이 표시되고, 설정한 날짜에 따라 dday와 하단에 날짜도 잘 렌더링이되는 것을 보실 수 있습니다.

하지만 앱을 종료했다가 다시 켜게되면 설정했던 타이틀과 날짜가 초기화됩니다.

따라서 해당 정보들을 저장할 필요가 있는데. AsyncStorage를 이용해서 기기내에 저장하겠습니다.

react-native-async-storage.github.io/async-storage/docs/install/

 

Installation | Async Storage

### Get library

react-native-async-storage.github.io

위의 사이트에서 여러가지 사용법이나 설치방법을 알 수 있습니다.

먼저 yarn add @react-native-async-storage/async-storage로 라이브러리를 설치하겠습니다.

마찬가지로 metro server를 종료하고 설치하시는게 좋습니다.

설치가 완료되고 에러가 발생하는 경우도 있는데, 이때는 metro server를 종료하고 다시 react-native run-android로 실행해주세요.

//App.js
import React from 'react';
import { StyleSheet, Text, View, Image, TextInput,TouchableOpacity, ScrollView, ImageBackground} from 'react-native';

//AsyncStorage import
import AsyncStorage from '@react-native-async-storage/async-storage';

import Setting from './Setting.js';

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      dday: new Date(),
      ddayTitle: '',
      chatLog: [],
      settingModal: false,
    }
  }
  // 추가
  async UNSAFE_componentWillMount() {
      try {
        const ddayString = await AsyncStorage.getItem('@dday')
        if(ddayString == null){
          this.setState(
            {
              dday: new Date(),
              ddayTitle: '',
            }
          );
        } else {
          const dday = JSON.parse(ddayString);
          this.setState(
            {
              dday: new Date(dday.date),
              ddayTitle: dday.title,
            }
          );
        }
      } catch(e) {
        console.log("ERR");
      }
  }

우선 먼저 AsyncStorage를 import해주세요.

그리고 UNSAGE_componentWillMount()라는 함수를 작성해줍니다.

react native는 컴포넌트가 mount, update, unmount라는 크게 세가지의 상태가 있습니다.

componentWillMount라는 함수의 이름에서 볼 수 있듯이, 마운트가 되기전 호출되는 함수입니다.

마운트는 렌더링을 포함하는 단계인데 쉽게말해 렌더링되기 전에 호출되는 함수라고 생각하시면 됩니다.

async에 대해서 아직 잘 모르시는분은 developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/async_function를 읽어보시면 좋을 것 같습니다.

asyncStorage작업이 실패할 수 있기 때문에 try, catch문으로 감싸게됩니다.

//App.js

  async UNSAFE_componentWillMount() {
      try {
        const ddayString = await AsyncStorage.getItem('@dday');
        if(ddayString === null){
          this.setState(
            {
              dday: new Date(),
              ddayTitle: '',
            }
          );
        } else {
          const dday = JSON.parse(ddayString);
          this.setState(
            {
              dday: new Date(dday.date),
              ddayTitle: dday.title,
            }
          );
        }
      } catch(e) {
        console.log("ERR");
      }
  }

각 라인별 뜻하는 바를 보게되면, const ddayString = await AsyncStorage.getItem('@dday');는 AsyncStorage로 저장된 아이템중 '@dday'라는 키를 가진 아이템을 가져와서 ddayString에 저장하라는 뜻입니다.

AsyncStorage는 저장시 키를 설정하게됩니다. 따라서 불러올때 해당 키를 적어주면 키에 해당하는 정보를 불러올 수 있습니다.

또한 AsyncStorage는 string을 저장할 수 있는데, javascript객체또한 JSON형식으로 stringfy해서 저장할 수 있습니다.

저장된 내용을 불러오고 난 뒤, 불러온 내용이 null인지 검사합니다. (저장된 내용이 없는 첫 실행인 경우는 null이 반환될거에요)

만약 null이었다면 dday는 오늘날짜, ddayTitle은 빈 문자열로 설정합니다.

하지만 값이 있었다면, JSON.parse로 json형식의 문자열을 파싱해서 dday에 저장합니다. 

setState를 이용해서 App.js의 state인 dday와 ddayTitle에 해당하는 값들을 업데이트합니다.

이렇게 하면 이전에 저장해둔 값들을 실행시 불러와서 이용할 수 있습니다.

이제 세팅 모달에서 확인을눌렀을 때, AsyncStorage에 저장하는 코드를 작성하겠습니다.

//App.js

..생략

  makeDateString() {
    return this.state.dday.getFullYear() + '년 ' + (this.state.dday.getMonth()+1) + '월 ' + this.state.dday.getDate() + '일';
  }
  makeRemainString() {
    const distance = new Date().getTime() - this.state.dday.getTime();
    console.log(new Date(), this.state.dday,distance / (1000 * 60 * 60 * 24) )
    const remain = Math.floor(distance / (1000 * 60 * 60 * 24));
    if(remain < 0) {
      return 'D'+remain;
    } else if (remain > 0) {
      return 'D+'+remain;
    } else if (remain === 0) {
      return 'D-day';
    }
  }
  toggleSettingModal() {
    this.setState({
      settingModal: !this.state.settingModal
    })
  }

//async 추가
  async settingHandler(title, date) {
     this.setState({
       ddayTitle: title,
       dday: date,
     });
     
     //저장루틴 추가
    try {
      const dday = {
        title: title,
        date: date,
      }
      const ddayString = JSON.stringify(dday);
      await AsyncStorage.setItem('@dday', ddayString);
    } catch (e) {
      console.log(e);
    }
     this.toggleSettingModal();
   }
   ..생략

settingHandler가 확인을 누를때 동작하는 함수였습니다.

AsyncStorage의 setItem을 사용하기 위해 settingHandler를 async함수로 선언해주세요.

이후 이전에 불러오기 루틴과 반대의 루틴을 작성해주면 됩니다.

dday라는 title과 date를 담고있는 객체를 하나 생성합니다. 

해당 객체를 JSON.stringfy로 string으로 만들어주세요.

그렇게 만들어진 문자열을 AsyncStorage.setItem으로 key는 아까 불러올 때 사용했던것과 같은 '@dday'로 저장합니다.

여기까지 완료했으면, 앱을 종료한뒤 다시 열어도 설정했던 값들이 불러와지는 것을 볼 수 있습니다.

다음 글에서는 구현의 마지막인 채팅기능을 구현하겠습니다.

복사했습니다!