Skip to main content
사용자가 Google 계정으로 웹사이트에 로그인할 수 있도록 허용하면 다음과 같은 이점이 있습니다:
  1. 자체 인증 방식을 구현할 필요가 없습니다.
  2. 사용자의 이름과 연락처 정보를 쉽게 얻을 수 있습니다.
  3. 동일한 자격 증명을 사용하여 Sheets, Drive와 같은 사용자의 Google 리소스에 접근할 수 있습니다.
이 가이드에서는 Replit에서 Python과 Flask를 사용하여 이를 구현하는 방법을 안내합니다. 먼저 기본 OAuth 인증 설정 방법을 살펴보고, 그 후에 인증 결과로 얻은 자격 증명을 사용하여 사용자의 Google 리소스에 접근하는 방법을 알아보겠습니다.

OAuth 소개

Google 인증은 OAuth 표준을 기반으로 합니다. OAuth의 작동 방식은 다음과 같습니다:
  1. 웹사이트 어딘가에서 사용자를 로그인 페이지로 안내합니다.
  2. 사용자가 로그인 페이지로 이동하면, 웹사이트에 로그인 폼을 구현하는 대신 Google의 로그인 서비스로 리다이렉트하여 사용자를 로그인시킵니다.
  3. Google의 로그인 서비스가 사용자를 성공적으로 로그인시키면, 미리 정의한 URL(예: https://YOUR_DOMAIN/oauth2callback)로 사용자 및 로그인 세션과 관련된 정보를 전송하면서 웹사이트로 다시 리다이렉트합니다.
  4. 사용자의 로그인 정보를 사용하여 액세스 토큰을 추가로 획득합니다. 이 토큰은 프로필 정보, 스프레드시트, 문서 등 사용자의 리소스에 접근하는 데 사용할 수 있는 패스와 같습니다.

OAuth: 코드 보기

코드를 먼저 보고 싶으시다면, 아래 코드가 필요한 것입니다. 단, 모든 것이 동작하려면 Google Cloud Console에서 몇 가지 설정이 필요합니다. 이 부분은 다음 섹션에서 다룹니다. Flask 템플릿을 사용하여 새 Replit 앱을 만들고 main.py에 다음 코드를 넣으세요. 코드의 주석이 각 부분의 기능을 설명합니다:
from flask import Flask, redirect, session, url_for, request
import google_auth_oauthlib.flow
import json
import os
import requests

app = Flask('app')
# `FLASK_SECRET_KEY`는 세션에 사용됩니다. 임의의 문자열을 생성하여
# 시크릿으로 저장해야 합니다.
app.secret_key = os.environ.get('FLASK_SECRET_KEY') or os.urandom(24)

# `GOOGLE_APIS_OAUTH_SECRET`은 Google Cloud Credentials 패널에서
# 다운로드할 JSON 파일의 내용을 담고 있습니다. 다음 섹션 참조.
oauth_config = json.loads(os.environ['GOOGLE_OAUTH_SECRETS'])

# OAuth 플로우 구성 설정
oauth_flow = google_auth_oauthlib.flow.Flow.from_client_config(
    oauth_config,
    # scopes는 인증 후 사용자를 대신하여 접근할 API를 정의합니다
    scopes=[
        "https://www.googleapis.com/auth/userinfo.email",
        "openid",
        "https://www.googleapis.com/auth/userinfo.profile",
    ]
)

# 로그인 페이지의 진입점입니다. `authorization_url`에 위치한 Google 로그인 서비스로
# 리다이렉트합니다. `redirect_uri`는 Google 로그인 서비스가 이 앱으로 다시
# 리다이렉트하는 데 사용할 URI입니다.
@app.route('/signin')
def signin():
    # Replit 앱 내부에서는 http를 사용하지만 외부에서는 https로 접근하므로
    # redirect_uri가 이와 일치해야 하기 때문에 URL을 http에서 https로 변환합니다
    oauth_flow.redirect_uri = url_for('oauth2callback', _external=True).replace('http://', 'https://')
    authorization_url, state = oauth_flow.authorization_url()
    session['state'] = state
    return redirect(authorization_url)

# Google 로그인 서비스가 다시 리다이렉트하는 엔드포인트입니다. Google Cloud의
# API 자격 증명 패널에서 "Authorized redirect URIs"에 추가해야 합니다.
# 액세스 토큰을 요청하기 위해 Google 엔드포인트를 호출하고 사용자 세션에 저장합니다.
# 이후 이 액세스 토큰을 사용하여 사용자를 대신해 API에 접근할 수 있습니다.
@app.route('/oauth2callback')
def oauth2callback():
    if not session['state'] == request.args['state']:
        return 'Invalid state parameter', 400
    oauth_flow.fetch_token(authorization_response=request.url.replace('http:', 'https:'))
    session['access_token'] = oauth_flow.credentials.token
    return redirect("/")

# 앱의 홈 페이지입니다. 로그인하지 않은 경우 로그인하도록 안내하고,
# 이미 로그인한 경우 사용자 정보를 표시합니다.
@app.route('/')
def welcome():
    if "access_token" in session:
        user_info = get_user_info(session["access_token"])
        if user_info:
            return f"""
                Hello {user_info["given_name"]}!<br>
                Your email address is {user_info["email"]}<br>
                <a href="/logout">Log out</a>
            """
    return """
        <h1>Hello!</h1>
        <a href="/signin">Sign In via Google</a><br>
    """

# 유효한 액세스 토큰으로 userinfo API를 호출하여 사용자 정보를 가져옵니다.
# 이것은 액세스 토큰으로 사용자를 대신해 API에 접근하는 첫 번째 예시입니다.
def get_user_info(access_token):
    response = requests.get("https://www.googleapis.com/oauth2/v3/userinfo", headers={
       "Authorization": f"Bearer {access_token}"
   })
    if response.status_code == 200:
        user_info = response.json()
        return user_info
    else:
        print(f"Failed to fetch user info: {response.status_code} {response.text}")
        return None

@app.route('/logout')
def logout():
    session.clear()
    return redirect('/')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

OAuth 앱 / 클라이언트 설정

위 코드가 동작하려면 Google Cloud에서 다음 작업을 수행해야 합니다.
  1. Google Cloud 프로젝트 만들기 (아직 없는 경우).
  2. OAuth 동의 화면 구성하기.
  3. 앱의 OAuth 클라이언트 ID 만들기.

Google Cloud 프로젝트 만들기

이 작업에 사용할 Google Cloud 프로젝트가 이미 있다면 이 단계를 건너뛸 수 있습니다.
  1. Google Cloud Console로 이동합니다.
  2. Google Cloud 로고 옆의 프로젝트 선택기 드롭박스를 클릭합니다:
Google 프로젝트 드롭다운
  1. 기존 프로젝트를 선택하거나 “New Project”를 클릭하여 새 프로젝트를 만듭니다.
프로젝트 선택
  1. 새 프로젝트를 만드는 경우 프로젝트 이름을 입력하고 “Create”를 클릭합니다.
새 프로젝트
팝업에 새 프로젝트가 표시되면 “Select project”를 클릭하여 해당 프로젝트를 활성 프로젝트로 설정합니다.

OAuth 동의 화면 구성

프로젝트가 생겼으면 OAuth 동의 화면을 구성할 수 있습니다:
  1. OAuth 동의 화면으로 이동합니다.
OAuth 동의 화면
  1. 프로젝트 드롭다운에 원하는 프로젝트가 선택되어 있는지 확인합니다.
  2. “External”을 선택하여 Google 계정이 있는 모든 사용자가 앱에 로그인할 수 있도록 합니다. “Internal”은 조직 내 사람들만 허용합니다.
  3. “Create”를 클릭합니다.
  4. 앱 이름과 이 앱을 지원하는 담당자의 이메일을 입력합니다.
OAuth 동의 화면
  1. “Developer contact information” 아래에 이메일 주소를 입력합니다.
개발자 연락처 정보
  1. “Save and continue”를 클릭합니다.
  2. Scopes 화면에서 앱이 접근할 API를 추가할 수 있습니다. 기본 사용자 정보를 가져오는 API에는 이미 접근 권한이 있습니다.
OAuth 범위
지금은 그대로 두고 “Save and continue”를 클릭합니다. 9. Test Users에서 테스트 단계 동안 앱을 테스트할 사용자의 이메일을 추가해야 합니다.
테스트 사용자
“Add users”를 클릭합니다. 10. 하나 이상의 Google 이메일 주소를 추가하고 “Add”를 클릭합니다.
테스트 사용자
그런 다음 “Save and continue”를 클릭합니다. 11. 요약 화면을 검토합니다. 언제든지 돌아가서 각 단계를 편집할 수 있습니다.
테스트 사용자

앱의 OAuth 클라이언트 ID 만들기

마지막 단계입니다. OAuth가 동작하려면 앱의 OAuth 클라이언트 ID를 만들어야 합니다.
  1. Credentials로 이동합니다.
테스트 사용자
  1. “Create credentials”를 클릭합니다.
테스트 사용자
“OAuth client ID”를 선택합니다. 3. Application type으로 “Web application”을 선택하고 이 클라이언트 ID의 이름을 입력합니다.
테스트 사용자
  1. 이제 Flask Replit 앱으로 이동합니다. Shell을 열고 다음을 입력합니다: echo https://$REPLIT_DEV_DOMAIN/oauth2callback. 결과는 다음과 같이 보일 것입니다: https://81309e9b-c4df-48e0-a2c2-0a8d3c0e3162-00-35ppsa0tcuv6v.infra-staging.replit.dev/oauth2callback. 이 텍스트를 복사하여 폼 하단의 “Authorized redirect URIs”에 입력합니다.
테스트 사용자
나중에 앱을 게시할 때 https://YOUR_APP_DOMAIN/oauth2callback 항목을 추가해야 합니다.
  1. “Create”를 클릭합니다.
  2. “Download JSON”을 클릭합니다:
테스트 사용자
  1. Replit 앱으로 이동하여 Secrets 패널을 엽니다. GOOGLE_OAUTH_SECRETS라는 시크릿을 만들고 다운로드한 파일의 내용을 시크릿 값으로 붙여넣습니다.
테스트 사용자
수고하셨습니다! 여기까지 따라오셨다면 축하합니다! 이제 Flask 앱을 실행하고 테스트 사용자 Google 계정으로 로그인할 수 있습니다. 모든 Google 사용자가 앱을 사용할 수 있게 하려면 동의 화면 페이지로 돌아가 “Publish App”을 클릭해야 합니다. Sheets, Drive 등의 추가 Google API가 필요한 경우 검증 과정이 필요할 수 있습니다. 다음으로는 Sheets와 같은 Google API 연동 방법을 다룹니다. 더 나아가고 싶다면 계속 따라오세요.

Google Sheets API 설정

Google Sheets와 같은 Google API 연동을 추가하려면 먼저 앱에 해당 API를 활성화해야 합니다. 사용 가능한 API를 탐색할 수 있습니다. 예시로 Google Sheets를 사용하겠습니다.
  1. Google Sheets API 목록 페이지로 이동합니다.
  2. “Enable”을 클릭합니다.
끝입니다! 이 부분의 Google Cloud 설정은 여기까지입니다.

Google Sheets 연동: 코드 보기

먼저 원본 코드의 OAuth 플로우 섹션에서 모든 것을 그대로 두고, 범위 목록에 "https://www.googleapis.com/auth/spreadsheets.readonly"만 추가합니다:
# OAuth 플로우 구성 설정
oauth_flow = google_auth_oauthlib.flow.Flow.from_client_config(
    oauth_config,
    # scopes는 인증 후 사용자를 대신하여 접근할 API를 정의합니다
    scopes=[
        "https://www.googleapis.com/auth/userinfo.email",
        "openid",
        "https://www.googleapis.com/auth/userinfo.profile",
        "https://www.googleapis.com/auth/spreadsheets.readonly"
    ]
)
이제 googleapiclient.discovery 라이브러리로 Google API에 접근하는 방법은 먼저 액세스 토큰을 사용하여 Credentials 객체를 만들고, build 함수를 사용하여 호출 가능한 API 객체를 만드는 것입니다. Sheets API의 경우 다음과 같습니다:
credentials = google.oauth2.credentials.Credentials(token=session['access_token'])
service = build("sheets", "v4", credentials=credentials)
sheets_api = service.spreadsheets()
Sheets API를 실제로 사용하는 방법에 대해 몇 가지 헬퍼 함수를 만들었습니다:
# Google 스프레드시트 내의 모든 시트 가져오기
def get_sheets(sheets_api, spreadsheet_id) -> list[str]:
    result = sheets_api.get(spreadsheetId=spreadsheet_id).execute()
    return [sheet["properties"]["title"] for sheet in result["sheets"]]

# Google 스프레드시트 내 특정 시트의 데이터 가져오기
def get_sheet_data(sheets_api, spreadsheet_id, sheet_title) -> list[list[str]]:
    result = (
        sheets_api.values()
        .get(spreadsheetId=spreadsheet_id, range=sheet_title)
        .execute()
    )
    return result["values"]
위의 도움으로 Google 스프레드시트를 가져오는 POST 핸들러 엔드포인트를 다음과 같이 만들 수 있습니다:
@app.route("/import_spreadsheet", methods = ['POST'])
def import_spreadsheet():
    if 'access_token' not in session:
        return redirect('/signin')
    spreadsheet_id = request.form["spreadsheet_id"]
    credentials = google.oauth2.credentials.Credentials(token=session['access_token'])
    service = build("sheets", "v4", credentials=credentials)
    sheets_api = service.spreadsheets()
    try:
        sheets = get_sheets(sheets_api, spreadsheet_id)
        data_by_sheets = {}
        for sheet in sheets:
            data = get_sheet_data(sheets_api, spreadsheet_id, sheet)
            data_by_sheets[sheet] = data
    except googleapiclient.errors.HttpError as e:
        return f"upload failure"
    dirpath = os.path.join("static", "uploads", spreadsheet_id)
    filepath = os.path.join(dirpath, "data.json")
    os.makedirs(dirpath, exist_ok=True)
    with open(filepath, "w") as file:
        json.dump(data_by_sheets, file)
    return "upload success!"
전체 동작 코드는 다음과 같습니다:
from flask import Flask, redirect, session, url_for, request
import google_auth_oauthlib.flow
import json
import os
import requests
from googleapiclient.discovery import build
import googleapiclient.errors
import google.oauth2.credentials

app = Flask('app')
# `FLASK_SECRET_KEY`는 세션에 사용됩니다. 임의의 문자열을 생성하여
# 시크릿으로 저장해야 합니다.
app.secret_key = os.environ.get('FLASK_SECRET_KEY') or os.urandom(24)

# `GOOGLE_APIS_OAUTH_SECRET`은 Google Cloud Credentials 패널에서
# 다운로드할 JSON 파일의 내용을 담고 있습니다. 다음 섹션 참조.
oauth_config = json.loads(os.environ['GOOGLE_OAUTH_SECRETS'])

# OAuth 플로우 구성 설정
oauth_flow = google_auth_oauthlib.flow.Flow.from_client_config(
    oauth_config,
    # scopes는 인증 후 사용자를 대신하여 접근할 API를 정의합니다
    scopes=[
        "https://www.googleapis.com/auth/userinfo.email",
        "openid",
        "https://www.googleapis.com/auth/userinfo.profile",
        "https://www.googleapis.com/auth/spreadsheets.readonly"
    ]
)

# 로그인 페이지의 진입점입니다. `authorization_url`에 위치한 Google 로그인 서비스로
# 리다이렉트합니다. `redirect_uri`는 Google 로그인 서비스가 이 앱으로 다시
# 리다이렉트하는 데 사용할 URI입니다.
@app.route('/signin')
def signin():
    # Replit 앱 내부에서는 http를 사용하지만 외부에서는 https로 접근하므로
    # redirect_uri가 이와 일치해야 하기 때문에 URL을 http에서 https로 변환합니다
    oauth_flow.redirect_uri = url_for('oauth2callback', _external=True).replace('http://', 'https://')
    authorization_url, state = oauth_flow.authorization_url()
    session['state'] = state
    return redirect(authorization_url)

# Google 로그인 서비스가 다시 리다이렉트하는 엔드포인트입니다. Google Cloud의
# API 자격 증명 패널에서 "Authorized redirect URIs"에 추가해야 합니다.
# 액세스 토큰을 요청하기 위해 Google 엔드포인트를 호출하고 사용자 세션에 저장합니다.
# 이후 이 액세스 토큰을 사용하여 사용자를 대신해 API에 접근할 수 있습니다.
@app.route('/oauth2callback')
def oauth2callback():
    if not session['state'] == request.args['state']:
        return 'Invalid state parameter', 400
    oauth_flow.fetch_token(authorization_response=request.url.replace('http:', 'https:'))
    session['access_token'] = oauth_flow.credentials.token
    return redirect("/")

# 유효한 액세스 토큰으로 userinfo API를 호출하여 사용자 정보를 가져옵니다.
# 이것은 액세스 토큰으로 사용자를 대신해 API에 접근하는 첫 번째 예시입니다.
def get_user_info(access_token):
    response = requests.get("https://www.googleapis.com/oauth2/v3/userinfo", headers={
       "Authorization": f"Bearer {access_token}"
   })
    if response.status_code == 200:
        user_info = response.json()
        return user_info
    else:
        print(f"Failed to fetch user info: {response.status_code} {response.text}")
        return None

@app.route('/logout')
def logout():
    session.clear()
    return redirect('/')

# Google 스프레드시트 내의 모든 시트 가져오기
def get_sheets(sheets_api, spreadsheet_id) -> list[str]:
    result = sheets_api.get(spreadsheetId=spreadsheet_id).execute()
    return [sheet["properties"]["title"] for sheet in result["sheets"]]

# Google 스프레드시트 내 특정 시트의 데이터 가져오기
def get_sheet_data(sheets_api, spreadsheet_id, sheet_title) -> list[list[str]]:
    result = (
        sheets_api.values()
        .get(spreadsheetId=spreadsheet_id, range=sheet_title)
        .execute()
    )
    return result["values"]

# 스프레드시트 가져오기 폼 렌더링
@app.route("/import_spreadsheet_form")
def import_spreadsheet_form():
    return """
    <h3>Import Spreadsheet</h3>
    <form action="/import_spreadsheet" method="POST">
        <label>Spreadsheet ID</label>
        <input type="text" name="spreadsheet_id">

        <button type="submit">Import</button>
    </form>
    """

@app.route("/import_spreadsheet", methods = ['POST'])
def import_spreadsheet():
    if 'access_token' not in session:
        return redirect('/signin')
    spreadsheet_id = request.form["spreadsheet_id"]
    credentials = google.oauth2.credentials.Credentials(token=session['access_token'])
    service = build("sheets", "v4", credentials=credentials)
    sheets_api = service.spreadsheets()
    try:
        sheets = get_sheets(sheets_api, spreadsheet_id)
        data_by_sheets = {}
        for sheet in sheets:
            data = get_sheet_data(sheets_api, spreadsheet_id, sheet)
            data_by_sheets[sheet] = data
    except googleapiclient.errors.HttpError as e:
        return f"upload failure"
    dirpath = os.path.join("static", "uploads", spreadsheet_id)
    filepath = os.path.join(dirpath, "data.json")
    os.makedirs(dirpath, exist_ok=True)
    with open(filepath, "w") as file:
        json.dump(data_by_sheets, file)
    return "upload success! Really!"

@app.route('/')
def welcome():
    if "access_token" in session:
        user_info = get_user_info(session["access_token"])
        if user_info:
            return f"""
            Hello {user_info["given_name"]}!<br>
            Your email address is {user_info["email"]}<br>
            <a href="/signin">Sign In to Google</a><br>
            <a href="/import_spreadsheet_form">Import a Sheet</a>
            """
    return """
    <h1>Welcome to Google Sheet Importer</h1>
    <a href="/signin">Sign In to Google</a><br>
    <a href="/import_spreadsheet_form">Import a Sheet</a>
    """

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
앱을 게시하는 경우 반드시:
  1. “Authorized redirect URIs”에 프로덕션 /oauth2callback URI를 추가하세요.
  2. 동의 화면 페이지로 이동하여 “Publish App”을 클릭하세요.
좋은 경험이 되셨기를 바랍니다. 앞으로의 여정도 즐겁게 이어나가시길 바랍니다.