한 걸음 두 걸음

android 안드로이드 ] 레드로핏 Retrofit2 실습 _ 서버 없이 본문

FrontEnd/Android

android 안드로이드 ] 레드로핏 Retrofit2 실습 _ 서버 없이

언제나 변함없이 2019. 12. 3. 19:02
반응형

https://onepinetwopine.tistory.com/262 이론부분 포스팅입니다.

Retrofit 실습

이 실습은
첫 번째로 자신이 가진 서버 없이 사용해보기
두 번째로 자신이 구현한 서버로 사용(https://onepinetwopine.tistory.com/512)해보기로%ED%95%B4%EB%B3%B4%EA%B8%B0%EB%A1%9C) 구성하였습니다.

환경설정

Manifest에

<uses-permission android:name="android.permission.INTERNET"></uses-permission>

를 추가해줍니다.

HTTP통신이니 인터넷에 접속하기 위한 접근 권한을 허락해주는 것입니다.

Gradle(:APP)에

     implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'

를 추가해줍니다.

여기서 Gson, Jackson, Moshi 등은 다른 유형을 지원하기 위한 변환기입니다. 여섯 모듈은 사용자의 편의를 위해 인기 직렬화 라이브러리를 적용시킬 수 있습니다.

  • Gson:com.squareup.retrofit2:converter-gson
  • Jackson:com.squareup.retrofit2:converter-jackson
  • Moshi:com.squareup.retrofit2:converter-moshi
  • Protobuf:com.squareup.retrofit2:converter-protobuf
  • Wire:com.squareup.retrofit2:converter-wire
  • Simple XML:com.squareup.retrofit2:converter-simplexml

Gson은 Java 객체를 JSON 표현으로 변환하는 데 사용할 수있는 Java 라이브러리입니다. 저는 GSON Converter를 추가해주도록 하겠습니다. (레퍼런스 https://github.com/google/gson) 참고로 2019.12월 현재 레트로핏은 2.6.2, GSON은 2.8.6 버전까지 나와있습니다.

이를 사용하는 이유는 직렬화 / 역직렬화 때문입니다. 데이터를 전송할 때 객체를 연속적인 데이터스트림으로 변형하여 보내도록 합니다. 역직렬화는 반대로 스트림으로 온 데이터를 다시 객체로 복원시키는 것입니다.

이는 Serializable인터페이스를 상속받음으로써 객체를 직렬화할 수 있게 됩니다.(transient를 이용하면 직렬화에서 일부 제외합니다.) 단, 클래스 내부에서 타 클래스 객체를 사용하는 경우 타 클래스도 Serializable한 클래스여야합니다. 이는 https://docs.oracle.com/javase/9/docs/api/java/math/package-summary.html 에서 별도로 확인하셔야합니다. 커스텀클래스라면 Serializable시켜주면 되겠죠?)

여기까지 Retrofit을 위한 환경을 설정하고 본격적으로 코딩을 해볼께요.

01. 개인서버없이 Retrofit 실습하기

최대한 간단하게 실습을 해보겠습니다ㅎㅎ 안드로이드스튜디오를 켜기만 해도 만날 수 있는 Hello world를 클릭하면 Retrofit2로 데이터를 GET방식으로 받아오도록 만들겠습니다. 이 때 사용할 서버는 github의 api입니다. 제가 만든 서버를 활용하는 것이 아니므로, 다른 서버를 사용해야겠죠? github api는 호출시 JSON으로 응답합니다.

https://api.github.com/

https://api.github.com/repos/square/retrofit/contributors

를 호출하면 github에서 제공하는 api의 모든 url을 확인할 수 있도록 json을 받을 수 있습니다.(들어가보시면 확인하실 수 있어요~) 이를 이용해볼께요.

MainActivity.java

코드 먼저 전체 적고 설명하겠습니다.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView t = (TextView)findViewById(R.id.textview_retrofit);

        t.setOnClickListener(v->{
            GitHubService gitHubService = GitHubService.retrofit.create(GitHubService.class);
            final Call<List<Contributor>> call = gitHubService.repoContributors("square", "retrofit");
            new NetworkCall().execute(call);
        });

    }

    private class NetworkCall extends AsyncTask<Call, Void, String> {
        @Override
        protected String doInBackground(Call... params) {
            try {
                Call<List<Contributor>> call = params[0];
                Response<List<Contributor>> response = call.execute();
                return response.body().toString();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(String result) {
            final TextView textView = (TextView) findViewById(R.id.textview_retrofit);
            textView.setText(result);
        }
    }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textview_retrofit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

GitHubService.java

public interface GitHubService {
    @GET("repos/{owner}/{repo}/contributors")
    Call<List<Contributor>> repoContributors(
            @Path("owner") String owner,
            @Path("repo") String repo);

    public static final Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build();
}

Contributor.java

public class Contributor {
    String login;
    String html_url;

    int contributions;

    @Override
    public String toString() {
        return login + " (" + contributions + ")";
    }

}

설명

TextView t = (TextView)findViewById(R.id.textview_retrofit);

텍스트뷰를 객체화시켜주고,

 t.setOnClickListener(v->{
            //레트로핏 이벤트 등록예정
        });

setOnClickListener를 달아주었습니다.(람다 표현식으로 JAVA8부터 사용할 수 있습니다.) 텍스트 클릭 시

  GitHubService gitHubService = GitHubService.retrofit.create(GitHubService.class);
  final Call<List<Contributor>> call = gitHubService.repoContributors("square", "retrofit");
  new NetworkCall().execute(call);

이 세 줄의 코드가 실행됩니다.
여기서
GitHubService.retrofit.create()함수는

public final class Retrofit
extends Object

구조를 가진 Retrofit클래스 내부에 있는 함수입니다.

<T> T    create(Class<T> service)
Create an implementation of the API endpoints defined by the service interface.

반환형은 제네릭이며 서비스 클래스를 매개변수로 받아오는 함수인 create()는
서비스인터페이스의 API앤드포인트의 구현체를 생성합니다.

여기서 엔드 포인트는 통신 채널의 한쪽 끝입니다. API를 통해 두 시스템이 상호작용 할 때 각 시스템을 앤드포인트라 합니다. 이는 서버의 URL이 될 수도 있고, 서비스가 될 수 도 있습니다. 위의 경우엔 서비스에 해당하겠네요.

이렇게 하는 이유는 레트로핏 클래스가 사용자의 API interface를 callable objects로 만드는 기능을 갖기 때문입니다.

GitHubService 인터페이스를 보면

  Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build();

레트로핏 객체가 생성되어 있고 이는 Gson에 의해 역직렬화하는 모습으로 구성되어 있습니다.

GitHubService 인터페이스의 상단에는

 @GET("repos/{owner}/{repo}/contributors")
    Call<List<Contributor>> repoContributors(
            @Path("owner") String owner,
            @Path("repo") String repo);

이렇게 되어 있는데, @GET어노테이션을 이용하면 쿼리 path 를 만들 수 있습니다.

public interface Call<T>
extends Cloneable

Cloneable을 상속받은 인터페이스입니다.(clone을 사용하기 위해 상속합니다. (that method to make a field-for-field copy of instances of that class.) T는 Successful response body type입니다.

Contributor 데이터 클래스를 리스트로 만든 객체를 받는 Call은 웹 서버에 요청을 보내고 응답을 반환하는 Retrofit 메서드를 호출하는 역할입니다. 각 호출은 자체 HTTP 요청 및 응답 쌍을 생성하며, clone()을 사용하여 폴링 혹은 재시도의 일환으로 동일한 웹 서버에 대해 동일한 매개 변수를 사용하여 여러 번 호출할 수 있습니다.

이를 사용하면 다르게 들어오는 ownerrepo를 대응시킬 수 있을 것으로 보입니다. 여기서는 square/retrofit으로 쓰였습니다.

이를 처리하기 위해 NetworkCall클래스를 내부 클래스로 활용하며, 이는 AsyncTask를 상속받습니다. 때문에 doInBackground()함수와 onPostExecute()함수를 오버라이딩하여 작성합니다.

호출 시

new NetworkCall().execute(call);

에서 살펴봐야 할 것은 execute(call)입니다. https://developer.android.com/reference/android/os/AsyncTask.html#execute-Params...-

execute()함수는 AsyncTask 클래스에 있는 함수로, UI스레드에서 호출이 가능하게 합니다.

execute
public final AsyncTask<Params, Progress, Result> execute (Params... params)

들어오는 매개 변수(call)를 사용하여 task을 실행합니다.
그리고 호출자가 참조를 유지할 수 있도록 task가 자신(AsyncTask)을 리턴합니다 (this).

이 기능은 플랫폼 버전에 따라 단일 백그라운드 스레드 또는 스레드 풀에 대해 대기열에서 작업을 예약합니다.
AsyncTasks는 여러 작업을 동시에 수행 할 수있는 스레드 풀로, 시작 Build.VERSION_CODES.HONEYCOMB하면 병렬 실행으로 인한 일반적인 응용 프로그램 오류를 피하기 위해 단일 스레드에서 작업이 다시 실행됩니다.

doInBackground 함수는

protected abstract Result doInBackground (Params... params)

execute()함수가 전달한 매개변수를 공유하여 받으며 백그라운드 스레드에서 실행됩니다.

onPostExecute 함수는

protected void onPostExecute (Result result)

doInBackground 함수가 실행된 후 doInBackground의 반환값인 Result를 매개변수로 받습니다.

그러므로 doInBackground 에서

  Call<List<Contributor>> call = params[0];
  Response<List<Contributor>> response = call.execute();
  return response.body().toString();

가져온 call을 실행시키고 받은 response의 body를 toString으로 반환하고 이를 onPostExecute에서 받아 UI에 setText로 반영합니다.

참고로

Response<T> execute() throws IOException
Synchronously send the request and return its response.

위와 같이 Response 반환형을 갖는 동기함수입니다. 리퀘스트를 보내고 리스폰스까지 받아 반환하지만 백그라운드에서 실행되므로 사용자에겐 비동기로 작용하겠죠?

여기까지 레트로핏 서버 없이 실행해봤습니다! ㅎㅎ
두 번째로는 레트로핏 서버가지고 해볼텐데, 포스팅이 생각보다 많이..길어진 것 같아서 다음 포스팅(https://onepinetwopine.tistory.com/512)으로%EC%9C%BC%EB%A1%9C) 작성하겠습니다


레퍼런스

retrofit을 만든 square사의 공식 페이지
https://square.github.io/retrofit/

Retrofit2 Github
https://github.com/square/retrofit

레트로핏2으로 HTTP 호출하기 - Realm 설명
https://academy.realm.io/kr/posts/retrofit2-for-http-requests/

Retrofit Java docs
https://square.github.io/retrofit/2.x/retrofit/

EndPoint에 대한 설명
https://smartbear.com/learn/performance-monitoring/api-endpoints/

Path에 대한 설명
https://judo0179.tistory.com/13

GET 쿼리 스트링이란? 생활코딩
https://opentutorials.org/course/2136/11945


https://api.github.com/repos/square/retrofit/contributors

API 결과

[
  {
    "login": "JakeWharton",
    "id": 66577,
    "node_id": "MDQ6VXNlcjY2NTc3",
    "avatar_url": "https://avatars0.githubusercontent.com/u/66577?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/JakeWharton",
    "html_url": "https://github.com/JakeWharton",
    "followers_url": "https://api.github.com/users/JakeWharton/followers",
    "following_url": "https://api.github.com/users/JakeWharton/following{/other_user}",
    "gists_url": "https://api.github.com/users/JakeWharton/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/JakeWharton/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/JakeWharton/subscriptions",
    "organizations_url": "https://api.github.com/users/JakeWharton/orgs",
    "repos_url": "https://api.github.com/users/JakeWharton/repos",
    "events_url": "https://api.github.com/users/JakeWharton/events{/privacy}",
    "received_events_url": "https://api.github.com/users/JakeWharton/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 1004
  },
  {
    "login": "swankjesse",
    "id": 133019,
    "node_id": "MDQ6VXNlcjEzMzAxOQ==",
    "avatar_url": "https://avatars3.githubusercontent.com/u/133019?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/swankjesse",
    "html_url": "https://github.com/swankjesse",
    "followers_url": "https://api.github.com/users/swankjesse/followers",
    "following_url": "https://api.github.com/users/swankjesse/following{/other_user}",
    "gists_url": "https://api.github.com/users/swankjesse/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/swankjesse/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/swankjesse/subscriptions",
    "organizations_url": "https://api.github.com/users/swankjesse/orgs",
    "repos_url": "https://api.github.com/users/swankjesse/repos",
    "events_url": "https://api.github.com/users/swankjesse/events{/privacy}",
    "received_events_url": "https://api.github.com/users/swankjesse/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 265
  },
  {
    "login": "pforhan",
    "id": 331925,
    "node_id": "MDQ6VXNlcjMzMTkyNQ==",
    "avatar_url": "https://avatars3.githubusercontent.com/u/331925?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/pforhan",
    "html_url": "https://github.com/pforhan",
    "followers_url": "https://api.github.com/users/pforhan/followers",
    "following_url": "https://api.github.com/users/pforhan/following{/other_user}",
    "gists_url": "https://api.github.com/users/pforhan/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/pforhan/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/pforhan/subscriptions",
    "organizations_url": "https://api.github.com/users/pforhan/orgs",
    "repos_url": "https://api.github.com/users/pforhan/repos",
    "events_url": "https://api.github.com/users/pforhan/events{/privacy}",
    "received_events_url": "https://api.github.com/users/pforhan/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 48
  },
  {
    "login": "eburke",
    "id": 63761,
    "node_id": "MDQ6VXNlcjYzNzYx",
    "avatar_url": "https://avatars0.githubusercontent.com/u/63761?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/eburke",
    "html_url": "https://github.com/eburke",
    "followers_url": "https://api.github.com/users/eburke/followers",
    "following_url": "https://api.github.com/users/eburke/following{/other_user}",
    "gists_url": "https://api.github.com/users/eburke/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/eburke/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/eburke/subscriptions",
    "organizations_url": "https://api.github.com/users/eburke/orgs",
    "repos_url": "https://api.github.com/users/eburke/repos",
    "events_url": "https://api.github.com/users/eburke/events{/privacy}",
    "received_events_url": "https://api.github.com/users/eburke/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 36
  },
  {
    "login": "NightlyNexus",
    "id": 4032667,
    "node_id": "MDQ6VXNlcjQwMzI2Njc=",
    "avatar_url": "https://avatars2.githubusercontent.com/u/4032667?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/NightlyNexus",
    "html_url": "https://github.com/NightlyNexus",
    "followers_url": "https://api.github.com/users/NightlyNexus/followers",
    "following_url": "https://api.github.com/users/NightlyNexus/following{/other_user}",
    "gists_url": "https://api.github.com/users/NightlyNexus/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/NightlyNexus/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/NightlyNexus/subscriptions",
    "organizations_url": "https://api.github.com/users/NightlyNexus/orgs",
    "repos_url": "https://api.github.com/users/NightlyNexus/repos",
    "events_url": "https://api.github.com/users/NightlyNexus/events{/privacy}",
    "received_events_url": "https://api.github.com/users/NightlyNexus/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 29
  },
  {
    "login": "dnkoutso",
    "id": 310370,
    "node_id": "MDQ6VXNlcjMxMDM3MA==",
    "avatar_url": "https://avatars2.githubusercontent.com/u/310370?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/dnkoutso",
    "html_url": "https://github.com/dnkoutso",
    "followers_url": "https://api.github.com/users/dnkoutso/followers",
    "following_url": "https://api.github.com/users/dnkoutso/following{/other_user}",
    "gists_url": "https://api.github.com/users/dnkoutso/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/dnkoutso/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/dnkoutso/subscriptions",
    "organizations_url": "https://api.github.com/users/dnkoutso/orgs",
    "repos_url": "https://api.github.com/users/dnkoutso/repos",
    "events_url": "https://api.github.com/users/dnkoutso/events{/privacy}",
    "received_events_url": "https://api.github.com/users/dnkoutso/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 26
  },
  {
    "login": "edenman",
    "id": 1063557,
    "node_id": "MDQ6VXNlcjEwNjM1NTc=",
    "avatar_url": "https://avatars2.githubusercontent.com/u/1063557?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/edenman",
    "html_url": "https://github.com/edenman",
    "followers_url": "https://api.github.com/users/edenman/followers",
    "following_url": "https://api.github.com/users/edenman/following{/other_user}",
    "gists_url": "https://api.github.com/users/edenman/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/edenman/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/edenman/subscriptions",
    "organizations_url": "https://api.github.com/users/edenman/orgs",
    "repos_url": "https://api.github.com/users/edenman/repos",
    "events_url": "https://api.github.com/users/edenman/events{/privacy}",
    "received_events_url": "https://api.github.com/users/edenman/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 24
  },
  {
    "login": "loganj",
    "id": 18877,
    "node_id": "MDQ6VXNlcjE4ODc3",
    "avatar_url": "https://avatars3.githubusercontent.com/u/18877?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/loganj",
    "html_url": "https://github.com/loganj",
    "followers_url": "https://api.github.com/users/loganj/followers",
    "following_url": "https://api.github.com/users/loganj/following{/other_user}",
    "gists_url": "https://api.github.com/users/loganj/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/loganj/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/loganj/subscriptions",
    "organizations_url": "https://api.github.com/users/loganj/orgs",
    "repos_url": "https://api.github.com/users/loganj/repos",
    "events_url": "https://api.github.com/users/loganj/events{/privacy}",
    "received_events_url": "https://api.github.com/users/loganj/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 17
  },
  {
    "login": "Noel-96",
    "id": 20171941,
    "node_id": "MDQ6VXNlcjIwMTcxOTQx",
    "avatar_url": "https://avatars3.githubusercontent.com/u/20171941?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/Noel-96",
    "html_url": "https://github.com/Noel-96",
    "followers_url": "https://api.github.com/users/Noel-96/followers",
    "following_url": "https://api.github.com/users/Noel-96/following{/other_user}",
    "gists_url": "https://api.github.com/users/Noel-96/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/Noel-96/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/Noel-96/subscriptions",
    "organizations_url": "https://api.github.com/users/Noel-96/orgs",
    "repos_url": "https://api.github.com/users/Noel-96/repos",
    "events_url": "https://api.github.com/users/Noel-96/events{/privacy}",
    "received_events_url": "https://api.github.com/users/Noel-96/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 16
  },
  {
    "login": "rcdickerson",
    "id": 63143,
    "node_id": "MDQ6VXNlcjYzMTQz",
    "avatar_url": "https://avatars1.githubusercontent.com/u/63143?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/rcdickerson",
    "html_url": "https://github.com/rcdickerson",
    "followers_url": "https://api.github.com/users/rcdickerson/followers",
    "following_url": "https://api.github.com/users/rcdickerson/following{/other_user}",
    "gists_url": "https://api.github.com/users/rcdickerson/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/rcdickerson/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/rcdickerson/subscriptions",
    "organizations_url": "https://api.github.com/users/rcdickerson/orgs",
    "repos_url": "https://api.github.com/users/rcdickerson/repos",
    "events_url": "https://api.github.com/users/rcdickerson/events{/privacy}",
    "received_events_url": "https://api.github.com/users/rcdickerson/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 14
  },
  {
    "login": "rjrjr",
    "id": 1884445,
    "node_id": "MDQ6VXNlcjE4ODQ0NDU=",
    "avatar_url": "https://avatars0.githubusercontent.com/u/1884445?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/rjrjr",
    "html_url": "https://github.com/rjrjr",
    "followers_url": "https://api.github.com/users/rjrjr/followers",
    "following_url": "https://api.github.com/users/rjrjr/following{/other_user}",
    "gists_url": "https://api.github.com/users/rjrjr/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/rjrjr/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/rjrjr/subscriptions",
    "organizations_url": "https://api.github.com/users/rjrjr/orgs",
    "repos_url": "https://api.github.com/users/rjrjr/repos",
    "events_url": "https://api.github.com/users/rjrjr/events{/privacy}",
    "received_events_url": "https://api.github.com/users/rjrjr/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 13
  },
  {
    "login": "kryali",
    "id": 174889,
    "node_id": "MDQ6VXNlcjE3NDg4OQ==",
    "avatar_url": "https://avatars1.githubusercontent.com/u/174889?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/kryali",
    "html_url": "https://github.com/kryali",
    "followers_url": "https://api.github.com/users/kryali/followers",
    "following_url": "https://api.github.com/users/kryali/following{/other_user}",
    "gists_url": "https://api.github.com/users/kryali/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/kryali/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/kryali/subscriptions",
    "organizations_url": "https://api.github.com/users/kryali/orgs",
    "repos_url": "https://api.github.com/users/kryali/repos",
    "events_url": "https://api.github.com/users/kryali/events{/privacy}",
    "received_events_url": "https://api.github.com/users/kryali/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 9
  },
  {
    "login": "adriancole",
    "id": 64215,
    "node_id": "MDQ6VXNlcjY0MjE1",
    "avatar_url": "https://avatars2.githubusercontent.com/u/64215?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/adriancole",
    "html_url": "https://github.com/adriancole",
    "followers_url": "https://api.github.com/users/adriancole/followers",
    "following_url": "https://api.github.com/users/adriancole/following{/other_user}",
    "gists_url": "https://api.github.com/users/adriancole/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/adriancole/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/adriancole/subscriptions",
    "organizations_url": "https://api.github.com/users/adriancole/orgs",
    "repos_url": "https://api.github.com/users/adriancole/repos",
    "events_url": "https://api.github.com/users/adriancole/events{/privacy}",
    "received_events_url": "https://api.github.com/users/adriancole/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 9
  },
  {
    "login": "Jawnnypoo",
    "id": 1459320,
    "node_id": "MDQ6VXNlcjE0NTkzMjA=",
    "avatar_url": "https://avatars3.githubusercontent.com/u/1459320?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/Jawnnypoo",
    "html_url": "https://github.com/Jawnnypoo",
    "followers_url": "https://api.github.com/users/Jawnnypoo/followers",
    "following_url": "https://api.github.com/users/Jawnnypoo/following{/other_user}",
    "gists_url": "https://api.github.com/users/Jawnnypoo/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/Jawnnypoo/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/Jawnnypoo/subscriptions",
    "organizations_url": "https://api.github.com/users/Jawnnypoo/orgs",
    "repos_url": "https://api.github.com/users/Jawnnypoo/repos",
    "events_url": "https://api.github.com/users/Jawnnypoo/events{/privacy}",
    "received_events_url": "https://api.github.com/users/Jawnnypoo/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 8
  },
  {
    "login": "holmes",
    "id": 59162,
    "node_id": "MDQ6VXNlcjU5MTYy",
    "avatar_url": "https://avatars3.githubusercontent.com/u/59162?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/holmes",
    "html_url": "https://github.com/holmes",
    "followers_url": "https://api.github.com/users/holmes/followers",
    "following_url": "https://api.github.com/users/holmes/following{/other_user}",
    "gists_url": "https://api.github.com/users/holmes/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/holmes/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/holmes/subscriptions",
    "organizations_url": "https://api.github.com/users/holmes/orgs",
    "repos_url": "https://api.github.com/users/holmes/repos",
    "events_url": "https://api.github.com/users/holmes/events{/privacy}",
    "received_events_url": "https://api.github.com/users/holmes/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 7
  },
  {
    "login": "JayNewstrom",
    "id": 713288,
    "node_id": "MDQ6VXNlcjcxMzI4OA==",
    "avatar_url": "https://avatars3.githubusercontent.com/u/713288?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/JayNewstrom",
    "html_url": "https://github.com/JayNewstrom",
    "followers_url": "https://api.github.com/users/JayNewstrom/followers",
    "following_url": "https://api.github.com/users/JayNewstrom/following{/other_user}",
    "gists_url": "https://api.github.com/users/JayNewstrom/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/JayNewstrom/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/JayNewstrom/subscriptions",
    "organizations_url": "https://api.github.com/users/JayNewstrom/orgs",
    "repos_url": "https://api.github.com/users/JayNewstrom/repos",
    "events_url": "https://api.github.com/users/JayNewstrom/events{/privacy}",
    "received_events_url": "https://api.github.com/users/JayNewstrom/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 7
  },
  {
    "login": "swanson",
    "id": 56947,
    "node_id": "MDQ6VXNlcjU2OTQ3",
    "avatar_url": "https://avatars3.githubusercontent.com/u/56947?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/swanson",
    "html_url": "https://github.com/swanson",
    "followers_url": "https://api.github.com/users/swanson/followers",
    "following_url": "https://api.github.com/users/swanson/following{/other_user}",
    "gists_url": "https://api.github.com/users/swanson/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/swanson/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/swanson/subscriptions",
    "organizations_url": "https://api.github.com/users/swanson/orgs",
    "repos_url": "https://api.github.com/users/swanson/repos",
    "events_url": "https://api.github.com/users/swanson/events{/privacy}",
    "received_events_url": "https://api.github.com/users/swanson/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 7
  },
  {
    "login": "crazybob",
    "id": 170788,
    "node_id": "MDQ6VXNlcjE3MDc4OA==",
    "avatar_url": "https://avatars2.githubusercontent.com/u/170788?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/crazybob",
    "html_url": "https://github.com/crazybob",
    "followers_url": "https://api.github.com/users/crazybob/followers",
    "following_url": "https://api.github.com/users/crazybob/following{/other_user}",
    "gists_url": "https://api.github.com/users/crazybob/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/crazybob/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/crazybob/subscriptions",
    "organizations_url": "https://api.github.com/users/crazybob/orgs",
    "repos_url": "https://api.github.com/users/crazybob/repos",
    "events_url": "https://api.github.com/users/crazybob/events{/privacy}",
    "received_events_url": "https://api.github.com/users/crazybob/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 6
  },
  {
    "login": "danrice-square",
    "id": 1952897,
    "node_id": "MDQ6VXNlcjE5NTI4OTc=",
    "avatar_url": "https://avatars0.githubusercontent.com/u/1952897?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/danrice-square",
    "html_url": "https://github.com/danrice-square",
    "followers_url": "https://api.github.com/users/danrice-square/followers",
    "following_url": "https://api.github.com/users/danrice-square/following{/other_user}",
    "gists_url": "https://api.github.com/users/danrice-square/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/danrice-square/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/danrice-square/subscriptions",
    "organizations_url": "https://api.github.com/users/danrice-square/orgs",
    "repos_url": "https://api.github.com/users/danrice-square/repos",
    "events_url": "https://api.github.com/users/danrice-square/events{/privacy}",
    "received_events_url": "https://api.github.com/users/danrice-square/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 5
  },
  {
    "login": "vanniktech",
    "id": 5759366,
    "node_id": "MDQ6VXNlcjU3NTkzNjY=",
    "avatar_url": "https://avatars0.githubusercontent.com/u/5759366?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/vanniktech",
    "html_url": "https://github.com/vanniktech",
    "followers_url": "https://api.github.com/users/vanniktech/followers",
    "following_url": "https://api.github.com/users/vanniktech/following{/other_user}",
    "gists_url": "https://api.github.com/users/vanniktech/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/vanniktech/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/vanniktech/subscriptions",
    "organizations_url": "https://api.github.com/users/vanniktech/orgs",
    "repos_url": "https://api.github.com/users/vanniktech/repos",
    "events_url": "https://api.github.com/users/vanniktech/events{/privacy}",
    "received_events_url": "https://api.github.com/users/vanniktech/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 5
  },
  {
    "login": "Turbo87",
    "id": 141300,
    "node_id": "MDQ6VXNlcjE0MTMwMA==",
    "avatar_url": "https://avatars2.githubusercontent.com/u/141300?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/Turbo87",
    "html_url": "https://github.com/Turbo87",
    "followers_url": "https://api.github.com/users/Turbo87/followers",
    "following_url": "https://api.github.com/users/Turbo87/following{/other_user}",
    "gists_url": "https://api.github.com/users/Turbo87/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/Turbo87/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/Turbo87/subscriptions",
    "organizations_url": "https://api.github.com/users/Turbo87/orgs",
    "repos_url": "https://api.github.com/users/Turbo87/repos",
    "events_url": "https://api.github.com/users/Turbo87/events{/privacy}",
    "received_events_url": "https://api.github.com/users/Turbo87/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 5
  },
  {
    "login": "naturalwarren",
    "id": 1113225,
    "node_id": "MDQ6VXNlcjExMTMyMjU=",
    "avatar_url": "https://avatars3.githubusercontent.com/u/1113225?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/naturalwarren",
    "html_url": "https://github.com/naturalwarren",
    "followers_url": "https://api.github.com/users/naturalwarren/followers",
    "following_url": "https://api.github.com/users/naturalwarren/following{/other_user}",
    "gists_url": "https://api.github.com/users/naturalwarren/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/naturalwarren/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/naturalwarren/subscriptions",
    "organizations_url": "https://api.github.com/users/naturalwarren/orgs",
    "repos_url": "https://api.github.com/users/naturalwarren/repos",
    "events_url": "https://api.github.com/users/naturalwarren/events{/privacy}",
    "received_events_url": "https://api.github.com/users/naturalwarren/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 5
  },
  {
    "login": "guptasourabh04",
    "id": 27951986,
    "node_id": "MDQ6VXNlcjI3OTUxOTg2",
    "avatar_url": "https://avatars1.githubusercontent.com/u/27951986?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/guptasourabh04",
    "html_url": "https://github.com/guptasourabh04",
    "followers_url": "https://api.github.com/users/guptasourabh04/followers",
    "following_url": "https://api.github.com/users/guptasourabh04/following{/other_user}",
    "gists_url": "https://api.github.com/users/guptasourabh04/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/guptasourabh04/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/guptasourabh04/subscriptions",
    "organizations_url": "https://api.github.com/users/guptasourabh04/orgs",
    "repos_url": "https://api.github.com/users/guptasourabh04/repos",
    "events_url": "https://api.github.com/users/guptasourabh04/events{/privacy}",
    "received_events_url": "https://api.github.com/users/guptasourabh04/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 4
  },
  {
    "login": "artem-zinnatullin",
    "id": 967132,
    "node_id": "MDQ6VXNlcjk2NzEzMg==",
    "avatar_url": "https://avatars0.githubusercontent.com/u/967132?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/artem-zinnatullin",
    "html_url": "https://github.com/artem-zinnatullin",
    "followers_url": "https://api.github.com/users/artem-zinnatullin/followers",
    "following_url": "https://api.github.com/users/artem-zinnatullin/following{/other_user}",
    "gists_url": "https://api.github.com/users/artem-zinnatullin/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/artem-zinnatullin/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/artem-zinnatullin/subscriptions",
    "organizations_url": "https://api.github.com/users/artem-zinnatullin/orgs",
    "repos_url": "https://api.github.com/users/artem-zinnatullin/repos",
    "events_url": "https://api.github.com/users/artem-zinnatullin/events{/privacy}",
    "received_events_url": "https://api.github.com/users/artem-zinnatullin/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 3
  },
  {
    "login": "chriscizek",
    "id": 17190266,
    "node_id": "MDQ6VXNlcjE3MTkwMjY2",
    "avatar_url": "https://avatars1.githubusercontent.com/u/17190266?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/chriscizek",
    "html_url": "https://github.com/chriscizek",
    "followers_url": "https://api.github.com/users/chriscizek/followers",
    "following_url": "https://api.github.com/users/chriscizek/following{/other_user}",
    "gists_url": "https://api.github.com/users/chriscizek/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/chriscizek/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/chriscizek/subscriptions",
    "organizations_url": "https://api.github.com/users/chriscizek/orgs",
    "repos_url": "https://api.github.com/users/chriscizek/repos",
    "events_url": "https://api.github.com/users/chriscizek/events{/privacy}",
    "received_events_url": "https://api.github.com/users/chriscizek/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 3
  },
  {
    "login": "codebutler",
    "id": 3827,
    "node_id": "MDQ6VXNlcjM4Mjc=",
    "avatar_url": "https://avatars2.githubusercontent.com/u/3827?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/codebutler",
    "html_url": "https://github.com/codebutler",
    "followers_url": "https://api.github.com/users/codebutler/followers",
    "following_url": "https://api.github.com/users/codebutler/following{/other_user}",
    "gists_url": "https://api.github.com/users/codebutler/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/codebutler/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/codebutler/subscriptions",
    "organizations_url": "https://api.github.com/users/codebutler/orgs",
    "repos_url": "https://api.github.com/users/codebutler/repos",
    "events_url": "https://api.github.com/users/codebutler/events{/privacy}",
    "received_events_url": "https://api.github.com/users/codebutler/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 3
  },
  {
    "login": "icastell",
    "id": 1678605,
    "node_id": "MDQ6VXNlcjE2Nzg2MDU=",
    "avatar_url": "https://avatars2.githubusercontent.com/u/1678605?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/icastell",
    "html_url": "https://github.com/icastell",
    "followers_url": "https://api.github.com/users/icastell/followers",
    "following_url": "https://api.github.com/users/icastell/following{/other_user}",
    "gists_url": "https://api.github.com/users/icastell/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/icastell/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/icastell/subscriptions",
    "organizations_url": "https://api.github.com/users/icastell/orgs",
    "repos_url": "https://api.github.com/users/icastell/repos",
    "events_url": "https://api.github.com/users/icastell/events{/privacy}",
    "received_events_url": "https://api.github.com/users/icastell/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 3
  },
  {
    "login": "jjNford",
    "id": 965425,
    "node_id": "MDQ6VXNlcjk2NTQyNQ==",
    "avatar_url": "https://avatars3.githubusercontent.com/u/965425?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/jjNford",
    "html_url": "https://github.com/jjNford",
    "followers_url": "https://api.github.com/users/jjNford/followers",
    "following_url": "https://api.github.com/users/jjNford/following{/other_user}",
    "gists_url": "https://api.github.com/users/jjNford/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/jjNford/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/jjNford/subscriptions",
    "organizations_url": "https://api.github.com/users/jjNford/orgs",
    "repos_url": "https://api.github.com/users/jjNford/repos",
    "events_url": "https://api.github.com/users/jjNford/events{/privacy}",
    "received_events_url": "https://api.github.com/users/jjNford/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 3
  },
  {
    "login": "ojh102",
    "id": 14901903,
    "node_id": "MDQ6VXNlcjE0OTAxOTAz",
    "avatar_url": "https://avatars2.githubusercontent.com/u/14901903?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/ojh102",
    "html_url": "https://github.com/ojh102",
    "followers_url": "https://api.github.com/users/ojh102/followers",
    "following_url": "https://api.github.com/users/ojh102/following{/other_user}",
    "gists_url": "https://api.github.com/users/ojh102/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/ojh102/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/ojh102/subscriptions",
    "organizations_url": "https://api.github.com/users/ojh102/orgs",
    "repos_url": "https://api.github.com/users/ojh102/repos",
    "events_url": "https://api.github.com/users/ojh102/events{/privacy}",
    "received_events_url": "https://api.github.com/users/ojh102/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 3
  },
  {
    "login": "f2prateek",
    "id": 843979,
    "node_id": "MDQ6VXNlcjg0Mzk3OQ==",
    "avatar_url": "https://avatars2.githubusercontent.com/u/843979?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/f2prateek",
    "html_url": "https://github.com/f2prateek",
    "followers_url": "https://api.github.com/users/f2prateek/followers",
    "following_url": "https://api.github.com/users/f2prateek/following{/other_user}",
    "gists_url": "https://api.github.com/users/f2prateek/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/f2prateek/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/f2prateek/subscriptions",
    "organizations_url": "https://api.github.com/users/f2prateek/orgs",
    "repos_url": "https://api.github.com/users/f2prateek/repos",
    "events_url": "https://api.github.com/users/f2prateek/events{/privacy}",
    "received_events_url": "https://api.github.com/users/f2prateek/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 3
  }
]x
반응형