728x90

ㅎㅎ 참으로 힘들었던 React Native에서 소셜 로그인 구현하기였다..

 

처음에 아무렇지 않게 PC기준으로 코드를 짜주었고, 또한 expo를 브라우저에서 가상으로 실행했을때도 너무 잘되었다. 

하지만 실제로 앱과 웹은 아예 다르게 보고 접근해야 한다는 사실을 잊고 있었다.

 

실제 웹에는 세가지 종류가 있는데 이는 해당 유튜브 영상을 참고하면 좋을듯 하다. 

위의 영상을 참고하면, 우리가 진행하는 앱은 하이브리드 앱으로, 내부는 웹뷰이지만 네이티브로 감싸져 있는 것이다.

 

이는 실제로 네이티브 코드로 구현된 네이티브 앱처럼 완벽하게 모바일에 젖어들 순 없지만, 우선 빠르게 앱을 만들고 테스트 하는 과정에서는 문제없이 진행될 수 있다. 

이게 메인이 아니니까 이정도로 넘어가고, 즉 우리는 "네이티브"로 둘러쌓인 "웹"을 구현하고 있으므로, 전반적인 환경을 네이티브 기준으로 생각해야 하는 것이다. 

 

따라서 웹과 통신하기 위해서는 기존의 웹->웹의 통신이 아닌 앱->웹의 방식으로 접근해야 했던 것이다. 

 

시작하기 전에

지금부터의 게시글들은 모두 내가 개인적으로 공부를 하며 정리한 부분이므로 실제와는 다를 가능성이 있음을 고려하면 좋겠다!

 

처음에 이를 어떻게 해결하면 좋을 지를 이해하기 위해 수많은 구글링을 했고, RN에서 소셜로그인 구현을 위해서는 크게 3가지 방법이 있다고 정리했다. 

(이는 단연 소셜로그인에 국한된 것이 아니다. 크게 생각해서 앱 -> 앱 통신을 위한 방법이라고 생각하면 될거 같다.)

이를 위해서는 내가 블로그에 이미 정리해둔 카카오 로그인의 앱키 개념에 대해 정리한 글을 참고하고 보면 이해가 더 잘 될 것이다.

 

1. 앱 -> 앱 통신하기

**우선적으로 expo 환경에서는 해당 방식이 불가능하다. 이것 때문에 많은 사람들이 expo를 택했다가 eject 한다는 글을 많이 보았다. 

카카오 developers사이트의 가이드만 보아도, 네이티브 SDK를 사용하기 위해서는 android 빌드 폴더 혹은 ios 빌드 폴더에 무언가를 설정해야 하는데, expo로 init한 프로젝트에는 이 폴더들이 생성되지 않는다...!

 

만약에 expo를 버린다면 실제로 react native 자체에서 카카오/네이버 소셜로그인을 위한 모듈을 제공하고 있으며, 

해당 블로그 글 참고하면 알 수있듯이, 뭔가 설정을 주르륵 하고 나서 간단하게 모듈을 통해 앱-> 앱으로 소셜로그인 요청과 통신이 가능하다.

 

이러한 편리함에도 불구하고 결국 expo를 계속 쥐고 있기로 결정했다.

그 이유는, 우리가 앱 자체에서 다른 앱과 통신해야하는 기능은 소셜로그인 뿐이기 때문이다. 이외에는 모두 모바일 하드웨어에 접근해야 하는 것이고, 하드웨어 접근이 더 메인이다. 이 부분에 있어서는 expo가 매우 편리하게 자체 라이브러리를 제공하고 있다.

서비스 자체의 성격으로 소셜로그인의 간편한 구현을 포기했다.

 

2. 웹앱 -> 앱 통신하기

일종의 expo를 쓴 RN프로젝트를 웹앱으로 취급하면(웹앱은 아니지만, 네이티브 SDK를 못쓰므로) javascript키를 써서 앱과 통신할수도 있다. 사실 해당 방식이 가장 권장 사항인 것 같다. 

그럼에도 불구하고 다음에 설명하겠지만, 우리는 웹앱 -> 웹의 방식을 택했다.

 

그 이유로는 시간상의 이유로 우선순위를 미뤄둔 것이다. 

해당 방식을 도입하면, 기존에 구현해둔 소셜로그인관련하여 server와 client코드를 완전히 바꾸어야 하고, 나도 새로운 방식을 공부해야 한다. 하지만 위에서도 설명했듯이 소셜로그인은 우리 서비스에서 완전히 메인은 아니다.

물론 어느 서비스에서도  쓰이는 중요한 기능이기 때문에 쓸데없는 기능은 아니므로, 메인 기능 구현을 완료하고 다시 코드를 리팩토링하는 방법으로 결정한 것이다.

 

따라서 메인 기능 구현 이후에, 소셜로그인을 javascript키로 구현하게되면, 해당 방식에 대해서 다시 자세하게 정리해보고자 한다.

해당 블로그 참고하여 구현해보고자 한다.

 

3. 웹앱 -> 웹 통신하기

위에서 설명한 바와 같이 우리는 해당 방법을 사용하기로 했다.

그렇다면 어떻게? 위에서 쭉 설명했듯이 앱은 웹과 다르다. 물론 개발 단계에서는 자체적인 url을 보유하고 있어서 마치 웹처럼 작동하는것 처럼 보이지만 실제로는 아니다. 

 

해당 통신에서 가장 메인은 소셜로그인을 다 구현하고 난 후, 서버에서 어떻게? 클라이언트로 데이터를 전달해주지? 이다.

해당 방법에 대한 해결책으로 우리는 웹뷰라이브러리와 모달 창을 활용하는 것으로 해결했다.

 

앱에서 웹으로 링크 이동을 하면 앱에서 완전히 벗어나게 되는데, 이때 다시 앱으로 돌아올때, redirect하는 것은 가능할지도 모르나, 실질적으로는 사용할 수 없는 방법이다. (실제로도 브라우저 개발 단계에서는 되었으나 에뮬레이터 돌리자마자 바로 안됐다.)

구글링을 해보니 해당 방법은 웹->웹에서만 가능하다고 한다.

 

해당 방법의 원리는 어떻게 보면 앱에서 웹을 띄우자이다. 실제로 소셜로그인 외에도 앱자체에서 웹을 띄워야 하는 서비스는 많고, 이를 위해서 react native와 expo 둘다 react-native-webview 기능을 제공한다.

(아 물론 javascript SDK를 쓰더라도 웹뷰 방식을 조금 도입해야 하는 듯하다)

 

해당 모듈은 웹에서는 지원하지않는 기능이므로 브라우저에서 테스트를 하면 실행되지 않는다. 에뮬레이터나 안드로이드 디바이스를 통해서만 확인하자.

-> expo 사이트 참고

 

react native 자체를 쓰면, 안드로이드와 ios 방식이 다르나, expo 환경에서는 두 코드 모두 동일하다. 

ios 방식을 썼지만, 내가 웹뷰 모달창으로 소셜로그인 구현에 많은 도움을 준 블로그이다...

 

 

RN(expo 환경) 카카오 소셜로그인 구현하기

 

1. LoginScreen/index.js 

-> 해당 파일에서 카카오 로그인하기 버튼을 클릭하면 소셜로그인 진행을 위한 카카오 로그인 웹을 담을 모달창을 띄우게 된다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import React, { Component } from 'react';
import { View, Image, StyleSheet, Text, TextInput, TouchableOpacity } from 'react-native';
 
import SocialWebviewModal from './SocialWebviewModal';
 
 
export default class LoginScreen extends Component {
  constructor(props) {
    super(props);
    this.state = {
      socialModalVisible: false,
      source: undefined,
    };
  }
  //소셜 로그인
  onPressSocial = async (social) => {
    this.setState({
      socialModalVisible: !this.state.socialModalVisible,
      source: `서버주소/oauth/${social}`,
    });
  };
 
  closeSocialModal = async () => {
    this.setState({
      socialModalVisible: !this.state.socialModalVisible,
    });
  };
 
 
  render() {
    return (
      ///다른 코드 생략
          <View>
            {this.state.source !== undefined ? (
              <SocialWebviewModal
                visible={this.state.socialModalVisible}
                source={this.state.source}
                closeSocialModal={this.closeSocialModal}
              />
            ) : null}
            <TouchableOpacity
              style={{
                //
              }}
              onPress={() => this.onPressSocial('kakao')}
            >
              <Text style={{ color: '#391B1B', fontSize: 18, fontWeight: 'bold' }}>
                카카오 로그인
              </Text>
            </TouchableOpacity>
      </View>
    );
  }
}
});
cs

 

2. 모달창을 띄우면 그 안에서 웹이 웹뷰라는 라이브러리를 통해서 렌더링 된다.

 

LoginScreen/SocialWebviewModal.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React from 'react';
import { StyleSheet } from 'react-native';
 
import Modal from 'react-native-modal';
import SocialWebview from './SocialWebview';
 
const SocialWebviewModal = (props) => {
  return (
    <Modal
      animationType="slide"
      transparent={true}
      visible={props.visible}
      style={styles.container}
    >
      <SocialWebview
        source={{ uri: props.source }}
        closeSocialModal={props.closeSocialModal}
      />
    </Modal>
  );
};
export default SocialWebviewModal;
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});
 
cs

 

3. 위의 모달창안에 구현될 웹뷰이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import React, { Component } from 'react';
import { WebView } from 'react-native-webview';
 
import { useAsyncStorage } from '@react-native-community/async-storage';
const { setItem } = useAsyncStorage('토큰 key 값');
 
export default class LoginScreen extends Component {
  constructor(props) {
    super(props);
  }
 
  INJECTED_JAVASCRIPT =
    '(function() {if(window.document.getElementsByTagName("pre").length>0){window.ReactNativeWebView.postMessage((window.document.getElementsByTagName("pre")[0].innerHTML));}})();';
 
  _handleMessage = async (event=> {
    console.log(JSON.parse(event.nativeEvent.data));
    let result = JSON.parse(event.nativeEvent.data);
    let success = result.message;
    if (success) {
      let userToken = result.Authorization;
      try {
        await setItem(userToken);
      } catch (e) {
        console.log(e);
      }
    }
    this.props.closeSocialModal();
  };
 
  render() {
    return (
      <WebView
        //ref={this._refWebView}
        originWhitelist={['*']}
        injectedJavaScript={this.INJECTED_JAVASCRIPT}
        source={this.props.source}
        javaScriptEnabled={true}
        onMessage={this._handleMessage}
      />
    );
  }
}
 
 
cs

-> INJECTED_JAVASCRIPT : 웹뷰에서 제공하는 기능이다. 이 함수는 무조건 string으로만 사용되어야 하기때문에 함수 구현을 위와 같이 문자열로 구현했다. 그렇다면 위의 함수는 어떤 의미 일까?

  (function() {

     if(window.document.getElementsByTagName("pre").length>0)   {window.ReactNativeWebView.postMessage((window.document.getElementsByTagName("pre")[0].innerHTML));

  }})();

  •  즉시 실행 함수로써, 웹뷰 내의 html 코드를 읽고 있다. "pre"라는 태그를 가진 것을 불러오는데, 해당 태그 내의 값이 있다면 이라는 조건으로 아래의 코드가 실행된다. 여기서 알 수 있듯이 웹뷰 내의 html에서 서버에서 보내주는 response 값들이 pre라는 태그 값으로 묶여서 보여짐을 알 수 있다. 즉 해당 코드는 response 값이 있다면으로 해석할 수 있다.
  • window.ReactNativeWebView.postMessage() : 이건 웹뷰가 제공하는 기능이다. 즉 response 값을 웹뷰를 감싸고 있는 앱으로 전달 해주라는 의미이다. 이것이 웹에서 클라이언트로 데이터를 전달하는 기능을 하는 것이다. 해당 함수를 통해서 서버에서 보내준 데이터가 웹뷰로 보여지고, 이 보여진 데이터가 앱으로 전달된다. 

->위의 INJECTED_JAVASCRIPT 함수를 통해 앱으로 전달된 값들은 <Webview> 태그 안의 onMessage에 담긴다. 따라서 onMessage에서 handleMessage 함수를 실행하면 이 함수안의 event 변수에 데이터가 담겨서 함수가 실행된다.

 

-> _handleMessage : 받은 데이터를 검사해서 앱 자체의 저장소에 토큰을 저장해주는 역할을 한다. 

    저장소에 토큰을 저장하면, index.js에서 만들어준 모달을 닫아주는 함수 props를 실행하여 모달창을 닫아서 웹뷰창을 없애준다.

 

소셜로그인 구현 자체는 해당 코드로 끝이 난다. 

 

물론 서버에서도 소셜서비스와의 통신과 같이 이루어 져야 하므로, 서버사이드의 코드는 블로그 내의 다른 게시글을 참고 하면 된다.

또한 소셜로그인을 진행한 이후, 저장소에 토큰을 저장하고 분기하는 과정을 거쳐야 하는데 이는 다른 게시글에서 추가로 정리했다.

728x90

+ Recent posts