Skip to main content

GitHub 앱을 사용하여 "GitHub로 로그인" 단추 빌드

이 자습서에 따라 Ruby 코드를 작성하여 GitHub App에 대한 웹 애플리케이션 흐름을 통해 사용자 액세스 토큰을 생성합니다.

소개

이 자습서에서는 웹 사이트의 "GitHub로 로그인" 단추를 빌드하는 방법을 보여 줍니다. 이 웹 사이트는 GitHub App을(를) 사용하여 웹 애플리케이션 흐름을 통해 사용자 액세스 토큰을 생성합니다. 그런 다음 이 웹 사이트는 사용자 액세스 토큰을 사용하여, 인증된 사용자를 대신해 API 요청을 수행합니다.

이 자습서에서는 Ruby를 사용하지만, 웹 개발에 사용되는 어떤 프로그래밍 언어든 웹 애플리케이션 흐름과 함께 사용할 수 있습니다.

웹 애플리케이션 흐름 및 사용자 액세스 토큰 정보

앱의 작업을 사용자에게 귀속하려면 앱에서 사용자 액세스 토큰을 사용해야 합니다. 자세한 내용은 "사용자를 대신하여 GitHub 앱으로 인증"을(를) 참조하세요.

GitHub App에 대한 사용자 액세스 토큰을 생성하는 방법에는 웹 애플리케이션 흐름과 디바이스 흐름의 두 가지가 있습니다. 앱에 웹 인터페이스에 대한 액세스 권한이 있는 경우 웹 애플리케이션 흐름을 사용해야 합니다. 앱에 웹 인터페이스에 대한 액세스 권한이 없는 경우 디바이스 흐름을 대신 사용해야 합니다. 자세한 내용은 "GitHub 앱에 대한 사용자 액세스 토큰 생성" 및 "GitHub 앱을 사용하여 CLI 빌드"을(를) 참조하세요.

필수 조건

이 자습서에서는 GitHub App을(를) 이미 등록했다고 가정합니다. GitHub App 등록에 대한 자세한 내용은 "GitHub 앱 등록"을(를) 참조하세요.

이 자습서를 따르기 전에 먼저 앱에 대한 콜백 URL을 설정해야 합니다. 이 자습서에서는 http://localhost:4567의 기본 URL이 있는 로컬 Sinatra 서버를 사용합니다. 예를 들어 로컬 Sinatra 애플리케이션의 기본 URL을 사용하려면 콜백 URL은 http://localhost:4567/github/callback이 될 수 있습니다. 앱을 배포할 준비가 되면 라이브 서버 주소를 사용하도록 콜백 URL을 변경할 수 있습니다. 앱의 콜백 URL을 업데이트하는 방법에 대한 자세한 내용은 "GitHub 앱 등록 수정" 및 "사용자 권한 부여 콜백 URL 정보"을(를) 참조하세요.

이 자습서에서는 Ruby 및 Ruby 템플릿 시스템인 ERB에 대한 기본적인 이해를 갖추었다고 가정합니다. 자세한 내용은 RubyERB를 참조하세요.

종속성 설치

이 자습서에서는 Ruby gem, Sinatra를 사용하여 Ruby로 웹 애플리케이션을 만듭니다. 자세한 내용은 Sinatra 추가 정보를 참조하세요.

이 자습서에서는 Ruby gem, dotenv를 사용하여 .env 파일에 저장된 값에 액세스합니다. 자세한 내용은 dotenv 추가 정보를 참조하세요.

이 자습서를 수행하려면 Ruby 프로젝트에 Sinatra 및 dotenv gem을 설치해야 합니다. 예를 들면 Bundler를 사용하여 설치할 수 있습니다.

  1. Bundler가 아직 설치되어 있지 않은 경우 해당 터미널에서 다음 명령을 실행합니다.

    gem install bundler
    
  2. 앱에 Gemfile이 없는 경우 터미널에서 다음 명령을 실행합니다.

    bundle init
    
  3. 앱에 Gemfile.lock이 없는 경우 터미널에서 다음 명령을 실행합니다.

    bundle install
    
  4. 해당 터미널에서 다음 명령을 실행하여 gem을 설치합니다.

    bundle add sinatra
    
    bundle add dotenv
    

클라이언트 ID 및 클라이언트 암호 저장

이 자습서에서는 환경 변수에 클라이언트 ID 및 클라이언트 암호를 저장하고 ENV.fetch를 사용하여 해당 정보에 액세스합니다. 앱을 배포할 때는 클라이언트 ID 및 클라이언트 암호를 저장하는 방법을 변경해야 합니다. 자세한 내용은 “클라이언트 암호 안전하게 저장”을 참조하세요.

  1. GitHub Enterprise Server의 페이지 오른쪽 위 모서리에서 프로필 사진을 클릭합니다.

  2. 계정 설정으로 이동합니다.

    • 개인 계정 소유한 앱의 경우 설정을 클릭합니다.
    • 조직이 소유한 앱의 경우:
      1. 사용자의 조직을 클릭합니다.
      2. 조직 오른쪽에서 설정을 클릭합니다.
  3. 왼쪽 사이드바에서 개발자 설정을 클릭합니다.

  4. 왼쪽 사이드바에서 GitHub Apps 을 클릭합니다.

  5. 작업하려는 GitHub App의 옆에 있는 편집을 클릭합니다.

  6. 앱의 설정 페이지에서 앱의 클라이언트 ID를 확인합니다. 다음 단계에서 .env 파일에 추가하게 됩니다. 클라이언트 ID는 앱 ID와 다릅니다.

  7. 앱의 설정 페이지에서 새 클라이언트 암호 생성을 클릭합니다. 다음 단계에서 .env 파일에 클라이언트 암호를 추가하게 됩니다.

  8. Gemfile과 동일한 수준에 .env라는 파일을 만듭니다.

  9. 프로젝트에 .gitignore 파일이 아직 없는 경우 파일과 Gemfile과 동일한 수준에 .gitignore 파일을 만듭니다.

  10. .gitignore 파일에 .env를 추가 합니다. 이렇게 하면 실수로 클라이언트 암호를 커밋하는 것을 방지할 수 있습니다. .gitignore 파일에 대한 자세한 내용은 "Ignoring files(파일 무시)"을(를) 참조하세요.

  11. .env 파일에 다음 내용을 추가합니다. YOUR_CLIENT_ID를 앱의 클라이언트 ID로 바꿉니다. YOUR_CLIENT_SECRET을 앱의 클라이언트 암호로 바꿉니다.

    CLIENT_ID="YOUR_CLIENT_ID"
    CLIENT_SECRET="YOUR_CLIENT_SECRET"
    

사용자 액세스 토큰을 생성하는 코드 추가

사용자 액세스 토큰을 가져오려면 사용자에게 앱에 권한을 부여하라는 메시지를 먼저 표시해야 합니다. 사용자가 앱에 권한을 부여하면 앱의 콜백 URL로 리디렉션됩니다. 콜백 URL에 대한 요청에는 code 쿼리 매개 변수가 포함됩니다. 앱이 해당 콜백 URL을 제공하라는 요청을 받으면 사용자 액세스 토큰에 대한 code 매개 변수를 교환할 수 있습니다.

다음 단계에서는 사용자 액세스 토큰을 생성하는 코드를 작성하도록 안내합니다. 최종 코드로 건너뛰려면 "전체 코드 예제"를 참조하세요.

  1. .env 파일과 동일한 디렉터리에서, 사용자 액세스 토큰을 생성하는 코드를 저장할 Ruby 파일을 만듭니다. 이 자습서에서는 app.rb 파일 이름을 지정합니다.

  2. app.rb의 맨 위에 다음 종속성을 추가합니다.

    Ruby
    require "sinatra"
    require "dotenv/load"
    require "net/http"
    require "json"
    

    sinatradotenv/load 종속성에는 이전에 설치한 gem이 사용됩니다. net/httpjson은 Ruby 표준 라이브러리의 일부입니다.

  3. 다음 코드를 app.rb에 추가하여 .env 파일에서 앱의 클라이언트 ID 및 클라이언트 암호를 가져옵니다.

    Ruby
    CLIENT_ID = ENV.fetch("CLIENT_ID")
    CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")
    
  4. 다음 코드를 app.rb에 추가하여 사용자에게 앱을 인증하라는 메시지를 표시하는 링크를 표시합니다.

    Ruby
    get "/" do
      link = '<a href="http(s)://HOSTNAME/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>'
      erb link
    end
    
  5. 다음 코드를 app.rb에 추가하여 앱의 콜백 URL에 대한 요청을 처리하고 요청에서 code 매개 변수를 가져옵니다. CALLBACK_URL을 앱의 콜백 URL로 바꿉니다. 단, 도메인은 제외합니다. 예를 들어 콜백 URL이 http://localhost:4567/github/callback이면 CALLBACK_URL/github/callback으로 바꿉니다.

    Ruby
    get "CALLBACK_URL" do
      code = params["code"]
      render = "Successfully authorized! Got code #{code}."
      erb render
    end
    

    현재 이 코드는 단순히 code 매개 변수와 함께 메시지를 렌더링합니다. 다음 단계에서는 이 코드 블록을 확장합니다.

  6. 필요에 따라 진행률을 확인합니다.

    이제 app.rb는 이렇게 바뀝니다. CALLBACK_URL은 도메인을 제외한 앱의 콜백 URL입니다.

    Ruby
    require "sinatra"
    require "dotenv/load"
    require "net/http"
    require "json"
    
    CLIENT_ID = ENV.fetch("CLIENT_ID")
    CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")
    
    get "/" do
      link = '<a href="http(s)://HOSTNAME/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>'
      erb link
    end
    
    get "CALLBACK_URL" do
      code = params["code"]
      render = "Successfully authorized! Got code #{code}."
      erb render
    end
    
    1. 터미널에서, app.rb가 저장된 디렉터리에서 ruby app.rb를 실행합니다. 로컬 Sinatra 서버가 시작됩니다.

    2. 브라우저에서 http://localhost:4567로 이동합니다. "GitHub로 로그인"이라는 텍스트가 있는 링크가 나타납니다.

    3. "GitHub로 로그인" 링크를 클릭합니다.

      앱에 권한을 부여하지 않은 경우 이 링크를 클릭하면 http(s)://HOSTNAME/login/oauth/authorize?client_id=CLIENT_ID로 이동됩니다. 여기서 CLIENT_ID는 앱의 클라이언트 ID입니다. 앱에 권한을 부여하라는 메시지를 사용자에게 표시하는 GitHub 페이지입니다. 단추를 클릭하여 앱에 권한을 부여하는 경우 앱의 콜백 URL로 이동됩니다.

      이전에 앱에 권한을 부여했고 권한 부여가 철회되지 않은 경우 권한 부여 프롬프트를 건너뛰고 콜백 URL로 직접 이동됩니다. 권한 부여 프롬프트를 보려면 이전 권한 부여를 철회하면 됩니다. 자세한 내용은 "GitHub 앱의 권한 부여 검토 및 취소"을(를) 참조하세요.

    4. 화면의 지시에 따라 "GitHub로 로그인" 링크를 클릭하여 콜백 URL 페이지로 이동한 다음 앱에 권한을 부여한 경우 "성공적으로 승인되었습니다! agc622abb6135be5d1f2 코드를 받았습니다"와 유사한 텍스트가 표시됩니다.

    5. Sinatra를 실행 중인 터미널에서 Ctrl+C를 입력하여 서버를 중지합니다.

  7. app.rb의 내용을 다음 코드로 바꿉니다. 여기서 CALLBACK_URL은 도메인을 제외한 앱의 콜백 URL입니다.

    이 코드는 사용자 액세스 토큰에 대한 code 매개 변수를 교환하는 논리를 추가합니다.

    • parse_response 함수는 GitHub API의 응답을 구문 분석합니다.
    • exchange_code 함수는 사용자 액세스 토큰에 대한 code 매개 변수를 교환합니다.
    • 콜백 URL 요청의 처리기가 이제 사용자 액세스 토큰에 대한 코드 매개 변수를 교환하기 위해 exchange_code를 호출합니다.
    • 이제 콜백 페이지에 토큰이 생성되었음을 나타내는 텍스트가 표시됩니다. 토큰 생성에 성공하지 못한 경우 페이지에 해당 오류가 표시됩니다.
    Ruby
    require "sinatra"
    require "dotenv/load"
    require "net/http"
    require "json"
    
    CLIENT_ID = ENV.fetch("CLIENT_ID")
    CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")
    
    def parse_response(response)
      case response
      when Net::HTTPOK
        JSON.parse(response.body)
      else
        puts response
        puts response.body
        {}
      end
    end
    
    def exchange_code(code)
      params = {
        "client_id" => CLIENT_ID,
        "client_secret" => CLIENT_SECRET,
        "code" => code
      }
      result = Net::HTTP.post(
        URI("http(s)://HOSTNAME/login/oauth/access_token"),
        URI.encode_www_form(params),
        {"Accept" => "application/json"}
      )
    
      parse_response(result)
    end
    
    get "/" do
      link = '<a href="http(s)://HOSTNAME/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>'
      erb link
    end
    
    get "CALLBACK_URL" do
      code = params["code"]
    
      token_data = exchange_code(code)
    
      if token_data.key?("access_token")
        token = token_data["access_token"]
    
        render = "Successfully authorized! Got code #{code} and exchanged it for a user access token ending in #{token[-9..-1]}."
        erb render
      else
        render = "Authorized, but unable to exchange code #{code} for token."
        erb render
      end
    end
    
  8. 필요에 따라 진행률을 확인합니다.

    1. 터미널에서, app.rb가 저장된 디렉터리에서 ruby app.rb를 실행합니다. 로컬 Sinatra 서버가 시작됩니다.
    2. 브라우저에서 http://localhost:4567로 이동합니다. "GitHub로 로그인"이라는 텍스트가 있는 링크가 나타납니다.
    3. "GitHub로 로그인" 링크를 클릭합니다.
    4. 화면에 지시하는 메시지가 표시되면 앱에 권한을 부여합니다.
    5. 화면의 지시에 따라 "GitHub로 로그인" 링크를 클릭하여 콜백 URL 페이지로 이동한 다음 앱에 권한을 부여한 경우 "성공적으로 승인되었습니다! 코드 4acd44861aeda86dacce를 받고 2zU5kQziE로 끝나는 사용자 액세스 토큰으로 교환했습니다"와 유사한 텍스트가 표시됩니다.
    6. Sinatra를 실행 중인 터미널에서 Ctrl+C를 입력하여 서버를 중지합니다.
  9. 이제 사용자 액세스 토큰이 있으므로 토큰을 사용하여 사용자를 대신해 API 요청을 수행할 수 있습니다. 예시:

    /user REST API 엔드포인트를 사용하여 사용자에 대한 정보를 가져오는 이 함수를 app.rb에 추가합니다.

    Ruby
    def user_info(token)
      uri = URI("http(s)://HOSTNAME/api/v3/user")
    
      result = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
        body = {"access_token" => token}.to_json
    
        auth = "Bearer #{token}"
        headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth}
    
        http.send_request("GET", uri.path, body, headers)
      end
    
      parse_response(result)
    end
    

    user_info 함수를 호출하고 사용자의 이름과 GitHub 로그인을 표시하도록 콜백 처리기를 업데이트합니다. CALLBACK_URL을 도메인을 제외한 앱의 콜백 URL로 바꾸어야 합니다.

    Ruby
    get "CALLBACK_URL" do
      code = params["code"]
    
      token_data = exchange_code(code)
    
      if token_data.key?("access_token")
        token = token_data["access_token"]
    
        user_info = user_info(token)
        handle = user_info["login"]
        name = user_info["name"]
    
        render = "Successfully authorized! Welcome, #{name} (#{handle})."
        erb render
      else
        render = "Authorized, but unable to exchange code #{code} for token."
        erb render
      end
    end
    
  10. 다음 섹션의 전체 코드 예제와 비교하여 코드를 확인합니다. 전체 코드 예제 아래의 "테스트" 섹션에서 설명하는 단계에 따라 코드를 테스트할 수 있습니다.

전체 코드 예제

이전 섹션에서 설명한 전체 코드 예제입니다.

CALLBACK_URL을 앱의 콜백 URL로 바꿉니다. 단, 도메인은 제외합니다. 예를 들어 콜백 URL이 http://localhost:4567/github/callback이면 CALLBACK_URL/github/callback으로 바꿉니다.

Ruby
require "sinatra"
require "dotenv/load"
require "net/http"
require "json"

CLIENT_ID = ENV.fetch("CLIENT_ID")
CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")

def parse_response(response)
  case response
  when Net::HTTPOK
    JSON.parse(response.body)
  else
    puts response
    puts response.body
    {}
  end
end

def exchange_code(code)
  params = {
    "client_id" => CLIENT_ID,
    "client_secret" => CLIENT_SECRET,
    "code" => code
  }
  result = Net::HTTP.post(
    URI("http(s)://HOSTNAME/login/oauth/access_token"),
    URI.encode_www_form(params),
    {"Accept" => "application/json"}
  )

  parse_response(result)
end

def user_info(token)
  uri = URI("http(s)://HOSTNAME/api/v3/user")

  result = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    body = {"access_token" => token}.to_json

    auth = "Bearer #{token}"
    headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth}

    http.send_request("GET", uri.path, body, headers)
  end

  parse_response(result)
end

get "/" do
  link = '<a href="http(s)://HOSTNAME/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>'
  erb link
end

get "CALLBACK_URL" do
  code = params["code"]

  token_data = exchange_code(code)

  if token_data.key?("access_token")
    token = token_data["access_token"]

    user_info = user_info(token)
    handle = user_info["login"]
    name = user_info["name"]

    render = "Successfully authorized! Welcome, #{name} (#{handle})."
    erb render
  else
    render = "Authorized, but unable to exchange code #{code} for token."
    erb render
  end
end

테스팅

이 자습서에서는 앱 코드가 app.rb라는 파일에 저장되고 로컬 Sinatra 애플리케이션 http://localhost:4567의 기본 URL을 사용한다고 가정합니다.

  1. 터미널에서, app.rb가 저장된 디렉터리에서 ruby app.rb를 실행합니다. 로컬 Sinatra 서버가 시작됩니다.

  2. 브라우저에서 http://localhost:4567로 이동합니다. "GitHub로 로그인"이라는 텍스트가 있는 링크가 나타납니다.

  3. "GitHub로 로그인" 링크를 클릭합니다.

    앱에 권한을 부여하지 않은 경우 이 링크를 클릭하면 http(s)://HOSTNAME/login/oauth/authorize?client_id=CLIENT_ID로 이동됩니다. 여기서 CLIENT_ID는 앱의 클라이언트 ID입니다. 앱에 권한을 부여하라는 메시지를 사용자에게 표시하는 GitHub 페이지입니다. 단추를 클릭하여 앱에 권한을 부여하는 경우 앱의 콜백 URL로 이동됩니다.

    이전에 앱에 권한을 부여했고 권한 부여가 철회되지 않은 경우 권한 부여 프롬프트를 건너뛰고 콜백 URL로 직접 이동됩니다. 권한 부여 프롬프트를 보려면 이전 권한 부여를 철회하면 됩니다. 자세한 내용은 "GitHub 앱의 권한 부여 검토 및 취소"을(를) 참조하세요.

  4. 화면의 지시에 따라 "GitHub로 로그인" 링크를 클릭하여 콜백 URL 페이지로 이동한 다음 앱에 권한을 부여한 경우 "성공적으로 승인되었습니다! 환영합니다. Mona Lisa(octocat)입니다"와 유사한 텍스트가 표시됩니다.

  5. Sinatra를 실행 중인 터미널에서 Ctrl+C를 입력하여 서버를 중지합니다.

다음 단계

클라이언트 암호를 안전하게 저장

앱의 클라이언트 암호를 공개해서는 안 됩니다. 이 자습서에서는 클라이언트 암호를 gitignored .env 파일에 저장하고 ENV.fetch를 사용하여 값에 액세스했습니다. 앱을 배포할 때 클라이언트 암호를 저장하고 코드를 업데이트하여 값을 적절하게 가져오는 안전한 방법을 선택해야 합니다.

예를 들어 애플리케이션이 배포된 서버의 환경 변수에 비밀을 저장할 수 있습니다. Azure Key Vault와 같은 비밀 관리 서비스를 사용할 수도 있습니다.

배포의 콜백 URL 업데이트

이 자습서에서는 http://localhost:4567로 시작하는 콜백 URL을 사용했습니다. 하지만 http://localhost:4567은 Sinatra 서버를 시작할 때만 컴퓨터에서 로컬로 사용할 수 있습니다. 앱을 배포하기 전에, 프로덕션에서 사용하는 콜백 URL을 사용하도록 콜백 URL을 업데이트해야 합니다. 앱의 콜백 URL을 업데이트하는 방법에 대한 자세한 내용은 "GitHub 앱 등록 수정" 및 "사용자 권한 부여 콜백 URL 정보"을(를) 참조하세요.

여러 콜백 URL 처리

이 자습서에서는 단일 콜백 URL을 사용했지만 앱에는 최대 10개의 콜백 URL을 사용할 수 있습니다. 여러 콜백 URL을 사용하려는 경우:

  • 앱에 콜백 URL을 추가합니다. 콜백 URL 추가에 대한 자세한 내용은 "GitHub 앱 등록 수정"을(를) 참조하세요.
  • http(s)://HOSTNAME/login/oauth/authorize에 연결할 때 redirect_uri 쿼리 매개 변수를 사용하여 사용자를 원하는 콜백 URL로 리디렉션합니다. 자세한 내용은 "GitHub 앱에 대한 사용자 액세스 토큰 생성"을(를) 참조하세요.
  • 앱 코드에서 get "CALLBACK_URL" do에서 시작되는 시작 코드 블록과 유사하게 각 콜백 URL을 처리합니다.

추가 매개 변수 지정

http(s)://HOSTNAME/login/oauth/authorize에 연결할 때 추가 쿼리 매개 변수를 전달할 수 있습니다. 자세한 내용은 "GitHub 앱에 대한 사용자 액세스 토큰 생성"을(를) 참조하세요.

기존 OAuth 토큰과 달리 사용자 액세스 토큰은 범위를 사용하지 않으므로 scope 매개 변수를 통해 범위를 지정할 수 없습니다. 대신 세분화된 사용 권한을 사용합니다. 사용자 액세스 토큰에는 사용자와 앱 모두에 있는 권한만 있습니다.

앱의 요구 사항에 맞게 코드 조정

이 자습서에서는 인증된 사용자에 대한 정보를 표시하는 방법을 보여 주었지만 다른 작업을 수행하도록 이 코드를 조정할 수도 있습니다. 만들려는 API 요청에 대한 추가 권한이 앱에 필요한 경우 앱의 권한을 업데이트해야 합니다. 자세한 내용은 "GitHub 앱의 권한 선택"을(를) 참조하세요.

이 자습서에서는 모든 코드를 단일 파일에 저장했지만 함수와 구성 요소를 별도의 파일로 이동할 수 있습니다.

토큰을 안전하게 저장

이 자습서에서는 사용자 액세스 토큰을 생성합니다. 사용자 액세스 토큰의 만료 설정을 옵트아웃하지 않는 한 사용자 액세스 토큰은 8시간 후에 만료됩니다. 또한 사용자 액세스 토큰을 다시 생성할 수 있는 새로 고침 토큰을 받게 됩니다. 자세한 내용은 "사용자 액세스 토큰 새로 고침"을(를) 참조하세요.

GitHub의 API와 추가로 상호 작용하려는 경우 나중에 사용할 수 있도록 토큰을 저장해야 합니다. 사용자 액세스 토큰을 저장하거나 토큰을 새로 고치도록 선택하는 경우 안전하게 저장해야 합니다. 토큰을 공개해서는 안 됩니다.

자세한 내용은 "GitHub App을 만드는 모범 사례"을(를) 참조하세요.

모범 사례 준수

GitHub App을(를) 사용하는 모범 사례를 따르는 것을 목표로 해야 합니다. 자세한 내용은 "GitHub App을 만드는 모범 사례"을(를) 참조하세요.