개인 토이 프로젝트에 깃허브 로그인을 사용할 일이 생겨, 처음으로(!) Login API 사용에 도전해 보았다. 네아로나 카카오 로그인을 먼저 쓰게 될 줄 알았는데 사람 일은 모르는 일이다.
각설하고, 바로 시작해보자.

전체적인 방법은 깃허브에서 공식적으로 제공하는 문서를 그대로 따라가면 되지만 처음이라 그런지 어려움이 좀 있었다. 다른 SNS 로그인 API도 크게 다르지 않을 것이라 생각하기에 이 곳에 전체 과정을 기록해두려 한다.

1. 깃허브에 애플리케이션 등록하기


우선 내가 사용할 애플리케이션을 깃허브에 등록했다. [Settings] - [Developer settings] - [OAuth Apps]에서 [New OAuth App]을 클릭하여 새로운 앱을 생성해주었다.


Application name에는 애플리케이션 이름을, Homepage URL은 깃헙 저장소 주소를 넣어주었다.
Authorization callback URL의 경우,
애플리케이션 프로젝트의 [TARGETS] - [Info]의 [URL Types]를 추가해 아래 사진과 같이 간단히 callback url을 생성할 수 있다.



앱을 등록하고 나면 아래의 사진과 같이 client ID와 client Secret을 확인할 수 있는 화면이 나타난다. client Secret은 처음에는 생성되어 있지 않으므로 [Generate a new client secret]으로 새로운 키값을 생성시켜주도록 한다.






2. 사용자 권한 요청하기


모든 준비가 끝났다면, 이후로는 코딩의 영역이다. 간단히 로그인을 테스트하기 위해 화면에 로그인 버튼과 로그인이 되었는지 표시해주는 레이블만 앱 화면에 배치하였다.




로그인 버튼을 터치했을 때 사파리 (외부)앱으로 이동해 사용자의 권한을 요청해야 한다. 따라서 버튼의 IBAction 메서드를 다음과 같이 작성하였다.

@IBAction func loginGithub(_ sender: UIButton) {
    let clientID = ClientLogin.client_id
    let scope = ClientLogin.scope // repo user
    let urlString = ClientLogin.reqAuthUrl // https://github.com/login/oauth/authorize
    var components = URLComponents(string: urlString)!
    
    components.queryItems = [
        URLQueryItem(name: "client_id", value: clientID),
        URLQueryItem(name: "scope", value: scope)
    ]
    guard let url = components.url else { return }
    // UserInfoManager.delegate = self
    UIApplication.shared.open(url)
}

client_id와 client_secret이 다른 곳에서도 사용되었기에 (예를 들어 SceneDelegate…) 아예 ClientLogin struct를 생성하여 그곳에 static 상수 값으로 정의해두었다.
scope의 경우 프로그래머가 필요한 사용자 권한 범위를 나타내는 것인데 나의 경우 repo, user로 지정하였다.

해당 메서드를 작성하고 애플리케이션을 실행해보면 다음과 같이 사파리 앱으로 잘 넘어가는 것을 확인할 수 있다.(주석으로 처리해 둔 델리게이트 처리는 다음 단계에서 이어서 설명하겠다.)

사용자의 권한을 얻어 와서 다시 앱을 열면 깃허브는 code를 보내게 되는데, 이는 10분 후에 만료되므로 받아온 즉시 access token을 요청하도록 처리하였다. 이러한 code는 SceneDelegate 또는 AppDelegate에서 확인할 수 있다.

여기에선 SceneDelegate를 통해 받아온 code를 처리하였다.

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    if let url = URLContexts.first?.url {
        let code = url.absoluteString.split(separator: "=").last?.description ?? ""

        if !code.isEmpty {
            var component = URLComponents(string: ClientLogin.tokenReqUrl)
            // tokenReqUrl: https://github.com/login/oauth/access_token
            let required = [
                URLQueryItem(name: "client_id", value: ClientLogin.client_id),
                URLQueryItem(name: "client_secret", value: ClientLogin.client_secret),
                URLQueryItem(name: "code", value: code)
            ]
            component?.queryItems = required
            
            guard let tokenUrl = component?.url else { return }
            var request = URLRequest(url: tokenUrl)
            request.setValue("application/json", forHTTPHeaderField: "Accept")
            
            let session = URLSession(configuration: .default)
            let dataTask = session.dataTask(with: request) { (data, response, error) in
                let successRange = 200 ..< 300
                guard error == nil, let statusCode = (response as? HTTPURLResponse)?.statusCode, successRange.contains(statusCode) else { return } // 에러 없고, 네트워킹에 성공했는지
                guard let resultData = data else { return }
                guard let info = UserInfoManager.parseInfo(resultData) else { return }
                UserInfoManager.user = info // 받아온 token 값을 저장
                UserInfoManager.loginSuccessed() // 로그인 성공
            }
            
            dataTask.resume()
            
        }

    }
}


URLSession을 통해 받아온 token 값을 저장한 후 loginSuccessed() 함수를 실행하여 로그인 성공 후 애플리케이션에 변화를 주었다.

원래는 token 값을 이용해 다시 사용자 정보를 가져와야 하지만 URLSession을 사용하는 방식 자체는 같으므로 우선 생략하였다.


3. 애플리케이션 화면 변화 보여주기


로그인이 되었다면 사용자에게 어떤 퍼포먼스를 보여줘야 한다. 화면이 이동된다거나, 사용자 이름을 화면에 표시하여 로그인이 성공적으로 끝났음을 표시해주어야하는 것이다.

여담으로 이번 단계가 이 포스트를 작성하게 된 궁극적인 원인이라 할 수 있겠다.

애플리케이션 화면에 배치해두었던 레이블에 ‘로그인 성공’이라는 문자열을 띄우기 위해 token이 저장된 UserInfoManagerViewController를 연결하고, 데이터를 주고 받을 필요가 있었다.
따라서, LoginDelegate protocol을 작성하여 연결 매개체를 만들었다.

protocol LoginDelegate {
    func loginSucceessed()
}


UserInfoManagerLoginDelegate 타입의 delegate 변수를 생성함으로써 LoginDelegate 함수를 사용할 수 있도록 했다.

class UserInfoManager {
    static var delegate: LoginDelegate?
    // 생략

    static func loginSuccessed() {
        delegate?.loginSucceessed()
        print("Login Successed")
    }
}



한편 ViewController는 LoginDelegate를 상속함으로써 작성해야하는 loginSuccessed() 함수를 통해 애플리케이션 화면의 레이블을 변경하였다.
URLSession 내부에서 실행되고 있으므로 레이블을 변경하는 작업을 메인 스레드로 보내주어야 한다.

extension ViewController: LoginDelegate {
    func loginSucceessed() {
        DispatchQueue.main.async {
            self.userId.text = "Login Successed"
        }
    }
}



실행해보면 다음과 같이 잘 작동하는 것을 확인할 수 있다! 만세!💃


참고: https://docs.github.com/en/enterprise-server@2.21/developers/apps/authorizing-oauth-apps

https://zeddios.tistory.com/1102