UniTask를 사용한 사진 목록 검색 예제

jinwook4217·2021년 1월 6일
3
post-thumbnail

이 예제는 깃헙에서 다운받을 수 있습니다.

JungJinuk/photoSearchSample-UniTask

사진 목록 검색 API 를 호출하여 검색어의 사진 10장을 보여주는 간단한 예제입니다. UniTask 와 try-catch 를 실제 프로젝트에 적용하는 것에 초점을 맞췄습니다.

프로젝트 미리보기


프로젝트에서 사용한 것


UniTask

유니티에서 비동기 관련 처리를 최적화 시킨 오픈소스 라이브러리이다.

TotalJson

Json 파싱 패키지는 API 호출의 결과로 string 값이 전달되는데, json 형식으로 되어있기 때문에 사용했다. (만약 더 좋은 Json 파싱 패키지나 기술이 있다면 알려주세요)

Unsplash search API

Unsplash 웹페이지에서는 많은 종류의 사진들을 무료로 사용할 수 있는데, developer 페이지로 들어가면 사진들과 과련된 API 를 제공하고 있다. 무료 API 로 테스트 프로젝트는 충분히 만들어 볼 수 있다.

Beautiful Free Images & Pictures | Unsplash

Unsplash Image API | Free HD Photo API

UI 구성


상단에 검색어 입력 InputText 와 검색 버튼을 배치하고, 하단에는 검색 결과 사진이 격자로 나타날 수 있게 Grid 를 배치했다. Grid 를 ScrollRect 로 감싸고 Content Size Fitter 를 붙여 콘텐츠 양에 따라 자동으로 스크롤 할 수 있도록 했다. 이미지 텍스처가 입혀질 이미지 컨테이너는 RawImage 하나로 만들고 Prefab 으로 만들었다. 스크립트에 관련 UI 컴포넌트를 링크해주었다.

코드 분석


전체 로직 살펴보기

  1. 검색 버튼 클릭
  2. 검색된 사진 목록 제거
  3. 검색어를 파라미터로 API 를 호출해서 사진 목록 정보 받아오기
  4. 10개의 사진을 비동기 병렬 처리로 10개의 사진 목록 보여주기

검색 버튼을 눌렀을 때 로직 구현

검색어를 파라미터로 API 를 호출하는 로직이 비동기이기 때문에 OnClickSearch 함수는 async 를 붙여주었고, 이 함수에서 리턴 값은 없기 때문에 UniTaskVoid 를 작성해 주었다.

// 메인 로직
private async UniTaskVoid OnClickSearch(string term) {
    // 사진 목록 제거
    foreach (Transform photo in content) {
        Destroy(photo.gameObject);
    }

    // API 로부터 사진 목록 정보 받기
    var photosData = await GetPhotosData(term);
    if (photosData == null) {
        Debug.LogWarning("photos data is null");
        return;
    }

    // 사진 목록 보여주기 비동기 병렬 처리
    var photos = JSON.ParseString(photosData).GetJArray("results").AsJSONArray();
    foreach (var photo in photos) {
        var url = photo.GetJSON("urls").GetString("regular");
        LoadAndShowPhoto(url).Forget();
    }
}

검색 버튼을 눌렀을 때 로직 연결

뷰가 처음 열릴 때, 검색 버튼 클릭 이벤트에 delegate 를 연결해준다. 이때 OnClickSearch() 함수는 비동기가 아니므로 Forget()를 뒤에 붙여서 async 함수는 await 으로 대기하라는 경고를 없앤다. 그리고 이렇게 하면 함수 안에 있는 비동기 처리를 끝까지 기다리지 않는다. Forget() 에 관해서는 아래에서 다시 다루도록 하자.

private void Awake() {
    searchButton.onClick.AddListener(() => OnClickSearch(textInput.text).Forget());
}

검색어를 파라미터로 API 를 호출하고 사진 목록 정보 받아오기

비동기 함수를 호출하는 곳이 아니라 실제로 비동기 로직을 작성하는 곳에 try-catch 를 작성해주었다. 이유는 비동기 로직을 메인 로직에서 분리하기 위해서고, 메인 로직에 try-catch 가 쓰이면 코드 가독성이 안좋았다. 그리고 에러가 있을 경우 null 을 리턴하도록 했다. 메인 로직에서는 결과 값 null 체크를 하도록 코드를 작성했다.

private async UniTask<string> GetPhotosData(string term) {
    using (var req = UnityWebRequest.Get($"https://api.unsplash.com/search/photos?query={term}")) {
        try {
            req.SetRequestHeader("Authorization", "Client-ID S2F1QMER5i40nOj6vV_JUrGfK7e2l7Ue0f_MLUX5STQ");
            await req.SendWebRequest();
            return req.downloadHandler.text;
        }
        catch (Exception err) {
            Debug.LogError("GetPhotosData: " + err);
            return null;
        }
    }
}

10개의 사진 비동기 병렬 처리하기

비동기 작업 1, 2, 3, 4가 있다고 해보자. 비동기 작업을 1부터 4까지 1이 끝나길 기다렸다가 2를 실행하고, 2가 끝나길 기다렸다가 3을 실행하고, 3이 끝나길 기다렸다가 4를 실행한다면, 전체 작업이 끝나는 시간은 1, 2, 3, 4 모든 비동기 작업이 걸리는 시간을 합한 시간이다. 서로 종속성이 없는 비동기 작업이라면 굳이 이렇게 구현할 필요가 없다.

하지만, 모든 비동기 작업을 동시에 실행하면 사용자 입장에서는 가장 오래걸리는 1번 비동기 작업 시간 만큼만 기다리면 된다.

아래 두 코드는 10개의 사진 정보가 담긴 배열을 foreach 문을 돌면서 LoadAndShowPhoto(url) 비동기 작업을 실행하고 있다. 두 코드의 차이점은 무엇일까?

첫 번째 코드는 비동기 작업을 기다렸다 다음 비동기 작업을 실행하는 순서대로 직렬적으로 처리하고 있고,

두 번째 코드는 비동기 작업을 기다리지 않고 다음 비동기 작업을 실행하는 방식으로 병렬적으로 처리하고 있다.

// 사진 목록 보여주기 비동기 직렬 처리
var photos = JSON.ParseString(photosData).GetJArray("results").AsJSONArray();
foreach (var photo in photos) {
    var url = photo.GetJSON("urls").GetString("regular");
    await LoadAndShowPhoto(url);
}
// 사진 목록 보여주기 비동기 병렬 처리
var photos = JSON.ParseString(photosData).GetJArray("results").AsJSONArray();
foreach (var photo in photos) {
    var url = photo.GetJSON("urls").GetString("regular");
    LoadAndShowPhoto(url).Forget();
}

이해를 돕기 위해 아래 예제를 준비했다. 아래 코드의 OnClickDelay() 함수를 실행하면 실행 순서는 다음과 같다.

public void OnClickDelay() {
    Debug.Log("start");
    Delay(1000).Forget();
    Debug.Log("end");
}

public async UniTaskVoid Delay(int ms) {
    await UniTask.Delay(ms);
    Debug.Log($"delay {ms} ms!");
}
  1. "start"
  2. "end" (곧바로)
  3. ... 1초 뒤
  4. "delay 1000 ms!"

Delay() 함수 안에 비동기 처리가 들어있지만, Delay() 함수를 호출 할 때 Forget() 을 붙여줌으로써, 비동기 처리를 기다리지 않는다. 그래서 바로 아래에 있는 코드가 곧바로 실행된다. 그러나 Delay() 안에 있는 비동기 처리는 정상적으로 1초를 기다리고 다음 코드가 실행된다.

전체 코드


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Networking;
using Cysharp.Threading.Tasks;
using System;
using Leguar.TotalJSON;

public class Example : MonoBehaviour {
    public InputField textInput;
    public Button searchButton;
    public Transform content;
    public GameObject photoContainer;

    private void Awake() {
        searchButton.onClick.AddListener(() => OnClickSearch(textInput.text).Forget());
    }

    private async UniTaskVoid OnClickSearch(string term) {
        // 사진 목록 제거
        foreach (Transform photo in content) {
            Destroy(photo.gameObject);
        }

        // API 로부터 사진 목록 정보 받기
        var photosData = await GetPhotosData(term);
        if (photosData == null) {
            Debug.LogWarning("photos data is null");
            return;
        }

        // 사진 목록 보여주기 비동기 병렬 처리
        var photos = JSON.ParseString(photosData).GetJArray("results").AsJSONArray();
        foreach (var photo in photos) {
            var url = photo.GetJSON("urls").GetString("regular");
            LoadAndShowPhoto(url).Forget();
        }
    }

    private async UniTask<string> GetPhotosData(string term) {
        using (var req = UnityWebRequest.Get($"https://api.unsplash.com/search/photos?query={term}")) {
            try {
                req.SetRequestHeader("Authorization", "Client-ID S2F1QMER5i40nOj6vV_JUrGfK7e2l7Ue0f_MLUX5STQ");
                await req.SendWebRequest();
                return req.downloadHandler.text;
            }
            catch (Exception err) {
                Debug.LogError("GetPhotosData: " + err);
                return null;
            }
        }
    }

    private async UniTask LoadAndShowPhoto(string url) {
        // url 로부터 텍스쳐 가져오기
        var texture = await GetTexture(url);
        if (texture == null) {
            Debug.LogWarning("texture is null");
            return;
        }

        // 사진 컨테이너 생성
        var obj = await GetPhotoContainerInstance();
        if (obj == null) {
            Debug.LogWarning("photo container instance is null");
            return;
        }
        var photoContainer = Instantiate(obj, content);
        photoContainer.GetComponent<RawImage>().texture = texture;
    }

    private async UniTask<Texture> GetTexture(string url) {
        using (var req = UnityWebRequestTexture.GetTexture(url)) {
            try {
                await req.SendWebRequest();
                return DownloadHandlerTexture.GetContent(req);
            }
            catch (Exception err) {
                Debug.LogError("GetTexture error: " + err);
                return null;
            }
        }
    }

    private async UniTask<GameObject> GetPhotoContainerInstance() {
        try {
            var obj = await Resources.LoadAsync("PhotoContainer") as GameObject;
            return obj;
        }
        catch (Exception err) {
            Debug.LogError("GetPhotoContainerInstance error: " + err);
            return null;
        }
    }
}
profile
유니티 개발을 조금씩 해왔습니다.

0개의 댓글