티스토리 뷰

계속 네아로 네아로 하길래 뭔가 했는데 '네이버 아이디로 로그인'이다. 심지어 공식명칭임;;;;

 

[깃허브 링크]

 github.com/naver/naveridlogin-sdk-ios

 

[사전 준비 사항]

developers.naver.com/docs/common/openapiguide/appregister.md

 

[가이드 링크]

developers.naver.com/docs/login/ios/

 

+) 외주사에서 앱 등록 하고 'cliendId'랑 'clientSecret'을 알려달라고 해야한다. 자기가 직접 등록해야한다면 여기 에서 하자! 등록하다보면 '앱 다운로드 URL'을 입력해야 하는데, 아직 배포 전이라면 회사 홈페이지 등을 링크해도 괜찮다. 

 

그리고 알아야 할 부분이, 안드로이드는 로그인 콜백 처리(사용자 정보 받아오는 부분)까지 네이버 SDK에서 해주는데 iOS는 콜백 코드는 API콜을 직접 해줘야 한다. 왜그런지 모르겠다ㅠㅠ 안드 개발자분이 왜 SDK코드 안쓰세요? 했는데 없는걸 어케써요...ㅠ

 

0. Info.plist 설정

LSApplicationQueriesSchemes에

naversearchapp
naversearchthirdlogin

을 추가해준다. 이를 추가해주지 않으면 

 

-canOpenURL: failed for URL: "naversearchapp://" - error: "This app is not allowed to query for scheme naversearchapp" 에러가 나게 된다. 

 

1. url scheme 설정

 

 

 

 

2. podfile설정

pod 'naveridlogin-sdk-ios'

하고 pod install 해준다.

 

 

3. AppDelegate 설정하기

아니..2021년에도 아직도 objective-c예제밖에 없는거 실화냐?????? ㅠ 읽으면서 직접 스위프트로 바꿨다....

 

일단 방식이 크게 두가지가 있는데,  

첫 번째는 네이버 앱을 활성화해 인증하는 방식이고, 두 번째는 애플리케이션에서 SafariViewController를 실행해 인증하는 방식입니다. 네이버 앱으로 인증하는 방식과 SafariViewController에서 인증하는 방식을 모두 활성화하면 네이버 아이디로 로그인할 때 모바일 기기에 네이버 앱이 설치돼 있는지 확인합니다. 네이버 앱이 설치돼 있다면 네이버 앱으로 인증하고, 네이버 앱이 설치돼 있지 않으면 SafariViewController에서 인증합니다.

 

네이버 앱으로 하려면

NaverThirdPartyLoginConnection.getSharedInstance()?.isNaverAppOauthEnable = true

 

사파리로 하려면

NaverThirdPartyLoginConnection.getSharedInstance()?.isInAppOauthEnable = true

 

나는 둘다 할거라서 둘다 써줬다.

 

4. Constant 설정

 

NaverThirdPartyConstantsForApp.h 파일의 변수들을 바꿔줘야 한다. 이 파일을 못찾아서 한참 헤맸는데, 

 

이렇게 파인더에서 찾아서 열면 훨씬 편할듯 하다....

 

 

밑에 #define으로 정의된 변수 네개를 바꿔주면 된다. 

 

kServiceAppUrlScheme에는 1번에서 설정해준 값을 넣으면 되고

kConsumerKey랑 kConsumerSecret에는 'cliendId'랑 'clientSecret'을 넣으면 된다. 

kServiceAppName에는 Naver에 등록할때 썼던 앱 이름을 쓰면 된다. 

 

5. 다시 AppDelegate

지금 선언한 #define변수들을 맵핑해준다. 

// 네이버 간편로그인 init
    let instance = NaverThirdPartyLoginConnection.getSharedInstance()
    ///  네이버앱으로 로그인
    instance.isNaverAppOauthEnable = true
    /// 사파리로 로그인
    instance.isInAppOauthEnable = true

    instance?.serviceUrlScheme = kServiceAppUrlScheme // 앱을 등록할 때 입력한 URL Scheme
    instance?.consumerKey = kConsumerKey // 상수 - client id
    instance?.consumerSecret = kConsumerSecret // pw
    instance?.appName = kServiceAppName // app name

 

6. 여전히 AppDelegate

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    NaverThirdPartyLoginConnection.getSharedInstance()?.application(app, open: url, options: options)
        return true
  }

이것도 추가해준다. 

 

7. SceneDelegate

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        NaverThirdPartyLoginConnection
        .getSharedInstance()?
        .receiveAccessToken(URLContexts.first?.url)
    }

 

8. Delegate채택

여기부터는 SwiftUI를 쓰거나, UIKit을 쓰거나 마음대로 해도 된다. 

나는 SwiftUI+Combine을 프로젝트에서 사용중이어서, Coordinator을 만들어줬다. 

[참고 - SwiftUI를 안쓸 때 코드]

import Foundation
import UIKit
import NaverThirdPartyLogin
import Alamofire

class LoginViewController: UIViewController, NaverThirdPartyLoginConnectionDelegate {
    
    @IBOutlet var nameLabel: UILabel!
    @IBOutlet var emailLabel: UILabel!
    @IBOutlet var id: UILabel!
    
    let loginInstance = NaverThirdPartyLoginConnection.getSharedInstance()
    
    override func viewDidLoad() {
        loginInstance?.delegate = self
    }
    
    // 로그인에 성공한 경우 호출
    func oauth20ConnectionDidFinishRequestACTokenWithAuthCode() {
        print("Success login")
        getInfo()
    }
    
    // referesh token
    func oauth20ConnectionDidFinishRequestACTokenWithRefreshToken() {
        loginInstance?.accessToken
    }
    
    // 로그아웃
    func oauth20ConnectionDidFinishDeleteToken() {
        print("log out")
    }
    
    // 모든 error
    func oauth20Connection(_ oauthConnection: NaverThirdPartyLoginConnection!, didFailWithError error: Error!) {
        print("error = \(error.localizedDescription)")
    }
    
    @IBAction func login(_ sender: Any) {
        
        loginInstance?.requestThirdPartyLogin()
    }
    
    @IBAction func logout(_ sender: Any) {
        loginInstance?.requestDeleteToken()
    }
    
    // RESTful API, id가져오기
    func getInfo() {
      guard let isValidAccessToken = loginInstance?.isValidAccessTokenExpireTimeNow() else { return }
      
      if !isValidAccessToken {
        return
      }
      
      guard let tokenType = loginInstance?.tokenType else { return }
      guard let accessToken = loginInstance?.accessToken else { return }
        
      let urlStr = "https://openapi.naver.com/v1/nid/me"
      let url = URL(string: urlStr)!
      
      let authorization = "\(tokenType) \(accessToken)"
      
      let req = AF.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: ["Authorization": authorization])
      
      req.responseJSON { response in
        guard let result = response.value as? [String: Any] else { return }
        guard let object = result["response"] as? [String: Any] else { return }
        guard let name = object["name"] as? String else { return }
        guard let email = object["email"] as? String else { return }
        guard let id = object["id"] as? String else {return}
        
        print(email)
        
        self.nameLabel.text = "\(name)"
        self.emailLabel.text = "\(email)"
        self.id.text = "\(id)"
      }
    }
    
}

 

[SwiftUI를 쓸 때 코드]

좀 어려웠는데 UIViewRepresentable을 쓰고 싶지는 않았다. 내가 리턴 받는 뷰는 SwiftUI 뷰인데 굳이? 그래서 UIViewControllerRepresentable을 사용했다. 

 

결과 모델도 직접 만들어 줬다. 

3.4.5에 나와있다. 

 

일단 이게 데이터 파싱하는 모델이다. 받아오지 않을 데이터는 옵셔널로 선언해줘야 decoding error가 안생긴다. 

Alamofire쓰는 사람은 이렇게 안하고 그냥 Alamofire 이용해서 구현해도 된다. 

 

//
//  NaverLoginAPI.swift
//
//  Created by SweetDev on 2021/03/02.
//  Copyright © 2021 SweetDev. All rights reserved.
//

import Combine
import Foundation
/// 네이버 간편로그인 이후 데이터를 받아오기 위해서 사용되는 API
enum NaverLoginRouter {
  case naverLogin(tokenType: String, accessToken: String)

  func asURLRequest() throws -> URLRequest {
    let result: (path: String, parameter: [String: String], body: [String: Any], header: [String: String], method: HTTPType) = {
      switch self {
      case let .naverLogin(tokenType, accessToken):
        return ("", [:], [:], ["Authorization": "\(tokenType) \(accessToken)"], .get)
      }
    }()

    guard var urlComponent = URLComponents(string: "https://openapi.naver.com/v1/nid/me" + result.path) else { throw APIError.invalidEndpoint }
    urlComponent.queryItems = result.parameter.map {
      URLQueryItem(name: $0.key, value: $0.value)
    }
    guard let url = urlComponent.url else { throw APIError.invalidEndpoint }
    var request = URLRequest(url: url)
    request.httpMethod = result.method.rawValue

    // 헤더 설정
    request.addValue(result.header["Authorization"]!, forHTTPHeaderField: "Authorization")

    return request
  }
}

/// 네이버 간편 로그인
private func _naverLogin(tokenType: String, accessToken: String, session: URLSession = URLSession.shared) throws -> URLSession.DataTaskPublisher {
  let request = try NaverLoginRouter.naverLogin(tokenType: tokenType, accessToken: accessToken).asURLRequest()
  return session.dataTaskPublisher(for: request)
}

func naverLogin(tokenType: String, accessToken: String) throws -> AnyPublisher<response_naver_login, Error>? {
  return try? _naverLogin(tokenType: tokenType, accessToken: accessToken)
    .tryMap { try validate($0.data, $0.response) }
    .decode(type: response_naver_login.self, decoder: JSONDecoder())
    .eraseToAnyPublisher()
}

// 모델: https://developers.naver.com/docs/login/devguide/#3-4-5-접근-토큰을-이용하여-프로필-api-호출하기
struct response_naver_login: Codable {
  let resultcode: String
  let message: String
  let response: NaverLoginResponse
}

struct NaverLoginResponse: Codable {
  /// 동일인 식별 정보 - 동일인 식별 정보는 네이버 아이디마다 고유하게 발급되는 값입니다.
  let id: String
  /// 사용자 별명
  let nickname: String?
  /// 사용자 이름
  let name: String?
  /// 사용자 메일 주소
  let email: String
  /// F: 여성 M: 남성 U: 확인불가
  let gender: String?
  /// 사용자 연령대
  let age: String?
  /// 사용자 생일(MM-DD 형식)
  let birthday: String?
  /// 사용자 프로필 사진 URL
  let profile_image: String?
}

 

//
//  NaverLoginDelegateObserver.swift
//
//  Created by SweetDev on 2021/03/02.
//  Copyright © 2021 SweetDev. All rights reserved.
//

import Combine
import Foundation
import NaverThirdPartyLogin
import SwiftUI

struct NaverVCRepresentable: UIViewControllerRepresentable {
  static var loginInstance: NaverThirdPartyLoginConnection? = nil
  // 로그아웃시도 사용되어서 static으로 선언
  let vc = NaverViewController()
  var callback: (String) -> Void

  func makeUIViewController(context: Context) -> UIViewController {
    return vc
  }

  func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}

  func makeCoordinator() -> Coordinator {
    return Coordinator(vc: vc, callback: callback)
  }

  class Coordinator: NSObject, NaverThirdPartyLoginConnectionDelegate {
    @Published var cancellable: AnyCancellable?
    var callback: (String) -> Void
    
    init(vc: NaverViewController, callback: @escaping (String) -> Void) {
      self.callback = callback
      super.init()
      vc.delegate = self

      NaverVCRepresentable.loginInstance = NaverThirdPartyLoginConnection.getSharedInstance()
      NaverVCRepresentable.loginInstance?.delegate = self
      NaverVCRepresentable.loginInstance?.requestThirdPartyLogin()
    }

    // 로그인에 성공한 경우 호출
    func oauth20ConnectionDidFinishRequestACTokenWithAuthCode() {
      print("Success login")
      getInfo()
    }

    // referesh token
    func oauth20ConnectionDidFinishRequestACTokenWithRefreshToken() {
//      loginInstance?.accessToken
    }

    // 로그아웃
    func oauth20ConnectionDidFinishDeleteToken() {
      print("log out")
    }

    // 모든 error
    func oauth20Connection(_ oauthConnection: NaverThirdPartyLoginConnection!, didFailWithError error: Error!) {
      print("error = \(error.localizedDescription)")
    }

    func getInfo() {
      guard let isValidAccessToken = NaverVCRepresentable.loginInstance?.isValidAccessTokenExpireTimeNow() else { return }
      
      if !isValidAccessToken {
        return
      }

      guard let tokenType = NaverVCRepresentable.loginInstance?.tokenType else { return }
      guard let accessToken = NaverVCRepresentable.loginInstance?.accessToken else { return }
      cancellable = try? naverLogin(tokenType: tokenType, accessToken: accessToken)?
        .sink(receiveCompletion: { completion in
          switch completion {
          case let .failure(error):
            print("naver login failed!!!!!!!!")
            print(error)
          case .finished:
            print("DONE - postUserPublisher")
          }
        }, receiveValue: { response_naver_login in
          print("네이버 로그인 이메일 \(response_naver_login.response)")
          self.callback(response_naver_login.response.email)
        })
    }
  }

  typealias UIViewControllerType = UIViewController
}

class NaverViewController: UIViewController {
  weak var delegate: NaverThirdPartyLoginConnectionDelegate?
}

 

그리고 이런식으로 SNS Register View에서 콜해줬다. 

import SwiftUI
import WebKit

struct SNSRegisterScene: View {
  var body: some View {
    VStack {
      // 네이버 로그인 이후 액션
      NaverVCRepresentable { email in
        snsAccount = email
        print("==네이버 로그인 결과: \(email)")
      }
      .showIf(condition: joinMethod == .naver)
      .frame(height: 0)
      }
      // 그 밑에 만들어야할 버튼들
    }
 }
      

 

showIf는 아직 스유 뷰에서 if else가 안먹혀서 만들어준 extension이다. 

 

extension View {
  func showIf(condition: Bool) -> AnyView {
    if condition {
      return AnyView(self)
    } else {
      return AnyView(EmptyView())
    }
  }
}

 

 

[참고한 사이트]

ios-development.tistory.com/142

'macOS, iOS' 카테고리의 다른 글

[iOS] swift의 mutating은?  (0) 2021.02.26
[SwiftUI] Navigation  (0) 2021.02.25
[iOS] device 정보 가지고 오기  (0) 2021.02.25
[SwiftUI] ButtonStyle  (0) 2021.02.23
[iOS] 이미지 업로드 방법 - Alamofire 없이  (0) 2021.02.22
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
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
글 보관함