MEMO

[Expo] Eject 없이 네이버 아이디 로그인(네아로) 적용하기 본문

기타

[Expo] Eject 없이 네이버 아이디 로그인(네아로) 적용하기

by_dev 2019. 5. 7. 15:17

Expo를 이용해서 애플리케이션 개발을 진행 중에 네이버 아이디로 로그인(네아로)을 적용해야 했습니다.

React Native App 개발을 처음 하는거라서 더 간편한 Expo로 개발을 시작하였는데, 

Expo Eject 를 시킨다면 Expo로 시작한 장점이 사라질 것 같아서 Eject 없이 네아로 를 하는 방법을 생각해 보았습니다.

결론적으로, 저는 WebView 를 이용해서 Eject 없이 네아로를 구현하였으며, 일반적인 구현 방법보다는 프로세스가 어색할 수 있습니다.

 

0. 개발 환경

Expo (React Native) 를 이용한 App 개발 환경이며, 서버는 Spring Boot를 사용하고 있습니다.

1 . 네이버 개발자 센터에 App 설정

네아로를 구현하기 위해서는 네이버 개발자 센터에 App 등록을 하고 네아로 Redirect URL 등을 설정해야 합니다.

우선 '내 애플리케이션' 메뉴에 들어가서 Application을 등록하면 Client ID , Client Secret 값을 얻을 수 있습니다.  이제 API 설정 Tab에 들어가서 네이버 아이디로 로그인 Callback URL 을 설정합니다. 

네이버 개발자센터 내 애플리케이션 정보 화면

2. 프로 세스

A. App에서 네이버 아이디로 로그인 버튼을 클릭

B. 서버에서 네이버 로그인 URL 생성 후 App으로 전달

C. App 에서 사용자의 네이버 아이디 로그인 화면을 Web View로 사용자에게 노출

D. 네이버 개발자센터에서 설정한 Callback URL 로 Response 값 전달

E-1. 네이버 로그인 성공 시 회원 데이터 저장 또는 업데이트 후 상태 값 전달

E-2. 네이버 로그인 실패 시 실패 데이터 상태 값 전달

F. Web View 에서 Login 완료 버튼을 클릭하면, 가지고 있는 네이버 아이디 로그인 상태 값을 App으로 전달

3. 개발 소스

- 로그인 버튼 화면 소스(React login.js)

import React, {Component} from 'react';
import { View, Text, Platform, Image, ScrollView, WebView } from 'react-native'
import Button from '../../components/Button'
import Style from '../../assets/scss/myPage';
import { StackActions, NavigationActions } from 'react-navigation';

export default class SettingScreen extends Component{
    static navigationOptions = {
      title: 'login',
    };

  naverLogin = async() => {
    const uri = await this.props.getNaverUri();
    this.props.navigation.push('WebLogin',{
      uri : uri,
      login:this.insertLogin,
      loginFail:this.loginFail,
    })
  }

  insertLogin = () => {
    console.log("SettingScreen insertLogin")
  }

  loginFail = () => {
    console.log("SettingScreen loginFail")
    this.props.navigation.goBack();
    alert("로그인에 실패하였습니다. 잠시 후 다시 시도해주세요.");
  }

    render() {
        const { isLogin } = this.props.login;
        const { characterList, bookMarkList } = this.props.myPage;
        return (
            <View>
                <Button title={"Facebook Login"} onPress={this.facebookLogin}/>
                <Button title={"Naver Login"} onPress={this.naverLogin}/>
            </View>
          );
      }
}

- Naver Login Url 생성(Spring Boot)

public ResponseEntity<String> getNaverURL(String auth_type) {
        ResponseEntity<String> result = null;

        try{
            // CSRF 방지를 위한 상태 토큰 생성 코드
            SecureRandom random = new SecureRandom();
            // 상태 토큰으로 사용할 랜덤 문자열 생성
            String state =  new BigInteger(130, random).toString(32);
            String callback = URLEncoder.encode(NAVER_CALLBACK_URL, "UTF-8");

            String url = "https://nid.naver.com/oauth2.0/authorize?client_id="+NAVER_CLIENT_ID+"&response_type=code&redirect_uri="+callback+"&state="+state;

            if(auth_type!=null){
                url+= "&auth_type="+auth_type;
                url+="&response_type=code";
            }

            result = new ResponseEntity<>(url, HttpStatus.OK);
        }catch (Exception e){
            e.printStackTrace();
            result = new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }

        return result;
    }

- Callback URL 로 들어온 네이버 아이디로 로그인 Response 값 확인 및 로그인 처리 URL(Spring Boot)

public ResponseEntity<Object> naverLogin(String code, String state, String grant_type) {

        ResponseEntity<Object> entity = null;

        String access_token = "Bearer ";
        String refresh_token = "";

        try{
            Map<String,Object> getToken = getNaverToken(code, state, grant_type, null);

            if((int)getToken.get("responseCode") ==200) {
                //접근 토큰 발급 요청 성공

                Map<String,String> token =  new ObjectMapper().readValue(getToken.get("response").toString(), HashMap.class);
                logger.info("token = "+token.get("access_token"));
                access_token += token.get("access_token");

                String profileURL = "https://openapi.naver.com/v1/nid/me";
                URL profile = new URL(profileURL);
                HttpURLConnection profile_con = (HttpURLConnection) profile.openConnection();
                profile_con.setRequestMethod("POST");
                profile_con.setRequestProperty("Authorization", access_token);
                Map<String, Object> profileMap = apiService.br(profile_con);
                Object profile_response = profileMap.get("response");
                int profile_code = (int) profileMap.get("responseCode");
                if(profile_code == 200){
                    //프로필 가져오기 성공
                    Map<String,Object> memberMap =  new ObjectMapper().readValue(profile_response.toString(), HashMap.class);
                    Map<String,String> memberMapRes = (Map) memberMap.get("response");

                    if(memberMapRes.get("email") == null || memberMapRes.get("name") == null
                            || memberMapRes.get("nickname")==null){
                        //email 은 필수 값이기 때문에 null 인경우 사용자가 네이버 프로필 권한에 제공하지 않음을 선택한것으로, 다시 동의를 구해야 함.
                        String url = getNaverURL("reprompt").getBody();
                        entity = new ResponseEntity<>(url, HttpStatus.NOT_ACCEPTABLE);
                    }else{
                        Member member = new Member();
                        member.setReg_type(NAVER);
                        member.setSns_id(memberMapRes.get("id"));
                        member.setEmail(memberMapRes.get("email"));
                        member.setNickName(memberMapRes.get("nickname"));
                        member.setName(memberMapRes.get("name"));
                        member.setProfileURL(memberMapRes.get("profile_image"));
                        member.setSns_accessToken(token.get("access_token"));
                        member.setSns_refreshToken(token.get("refresh_token"));


                        Member getMember = memberMapper.getMember(member);
                        if(getMember == null){
                            memberMapper.insertMember(member);
                            getMember = memberMapper.getMember(member);
                        }else{
                            member.setMember_no(getMember.getMember_no());
                            memberMapper.updateProfile(member);
                            memberMapper.updateSnsToken(member);
                            getMember = member;
                        }
                        HttpHeaders headers = updateToken(getMember.getMember_no());
                        entity = new ResponseEntity<>(getMember, headers, HttpStatus.OK);
                    }

                }
            }else{
                entity = new ResponseEntity<>(HttpStatus.BAD_REQUEST);
            }
        }catch (Exception e){
            e.printStackTrace();
            entity = new ResponseEntity<>(HttpStatus.BAD_REQUEST);

        }

        return entity;
    }

- 로그인 완료 화면(Spring Boot login.html)

<!DOCTYPE html>

<html xmlns:th="www.thymeleaf.org">

<head> <meta charset="UTF-8" />
    <title></title>
</head>
<body style="flex:1;
    justify-content: center;
    vertical-align: middle;">
<div style="font-size: 50pt;justify-content: center;align-items: center;text-align: center;"
        th:if="${result.statusCodeValue == 200}">로그인에 성공하였습니다.</div>
<div th:unless="${result.statusCodeValue == 406}">
    <div style="font-size: 50pt;justify-content: center;align-items: center;text-align: center;" th:unless="${result.statusCodeValue == 200}">로그인에 실패하였습니다.</div>
    <button style="margin: 10px auto;
            display: flex;
            flex-direction: row;
            justify-content: space-between;
            flex-wrap: wrap;
            max-width: 320px;
            font-size: 40pt;"
            type="button" onclick="complete()">확인</button>
</div>

</body>

<script th:inline="javascript">
    var result = [[${result}]];
    window.onload = function(){
        console.log("hi!!!!",result);
        if(result.statusCodeValue === 406){
            window.location.href=result.body;
        }
    };

    function complete(){
        console.log("complete result");
        console.log(result);
        window.postMessage(JSON.stringify(result),"*");
    }
</script>

</html>

- WebView 화면 소스 (React Webview.js)

import React, {Component} from 'react';
import { View, Text, Platform, StyleSheet, Button, WebView } from 'react-native'
import { StackActions, NavigationActions } from 'react-navigation';

export default class WebLogin extends Component {

    componentWillMount(){
        this.setState({
            url:this.props.navigation.getParam("uri")
        });
    }

    webViewEnd = async(event) => {
        const result = JSON.parse(event.nativeEvent.data);
        console.log("result",result);
        if(result.statusCodeValue === 200) {
            //성공적 네이버 로그인/회원가입 완료
            //login 정보 저장 하기!
            const data = {
                token:token,
                nickName:nickName,
                profile:profileURL
            }
            console.log("data = ",data);
            await this.props.login(data);
            const resetAction = StackActions.reset({
                index: 0,
                actions: [NavigationActions.navigate({ routeName: 'Setting' })],
            });
            this.props.navigation.dispatch(resetAction);
        }else{
            //실패..
            this.props.navigation.goBack();
        }

    }

    render(){
        const uri = this.props.navigation.getParam("uri");
        console.log("[WebLogin] uri = ",this.state.url);
        return(
            <View style={styles.container}>
            <WebView
                ref={ref=> (this.webview = ref)}
                source={{uri:this.state.url}}
                useWebKit={true} 
                onMessage={(event)=>this.webViewEnd(event)}
            />
            </View>

        )
    }
  }

  const styles = StyleSheet.create({
      container:{
          flex:1,
      }
  })

'기타' 카테고리의 다른 글

[TDD] Test-Driven Development 테스트 주도 개발  (0) 2020.06.23
[Clean Code] 정리  (0) 2020.06.01
Comments