Carrot
본문 바로가기
Unity/멋쟁이사자처럼 부트캠프

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(88일차) - Object Pool을 활용한 Scroll View 실습 & 안드로이드 빌드

by 독기품은토끼 2025. 9. 24.
✅ 오늘의 학습 목표
1. Scroll View 만들기
2. 안드로이드 빌드

1. Scroll View

기존에 스크롤뷰는 사용한 적이 있었지만 스크롤뷰의 치명적인 단점은

많은 Content를 한꺼번에 Instantiate 하게되면 성능에 문제가 있다는 점이다.

이번 실습에서는 Object pool을 활용해서 보이는 셀만 유지하고

스크롤할 때만 셀을 재활용하도록 해주겠다.

 

[게임에서의 스크롤뷰 문제점 예시]

우리가 인벤토리를 구현할 때 일반적으로 아이템이 수십개만 되어도 스크롤뷰에 전부 넣어두면 게임 성능이 떨어지고 메모리 낭비가 발생한다.

특히 모바일 환경에서는 프레임 드랍으로 이어지기 쉽다.

그래서 Object pool을 활용해서 필요한 개수의 셀만 생성하고 스크롤할 때 재사용되도록 구현해주는 것이다.

 

1. 데이터 구조 정의

public struct Item
{
    public string imageFileName;
    public string title;
    public string subtitle;
}

 

Item은 스크롤뷰에 표시할 데이터의 단위이다.

이미지 이름, 제목, 부제목 이 세가지 정보를 담는다.

 

2. 데이터 표시

 

데이터를 표시할 Cell 프리팹을 만들어주었고

 

public class Cell : MonoBehaviour
{
    [SerializeField] private Image image;
    [SerializeField] private TMP_Text titleText;
    [SerializeField] private TMP_Text subtitleText;

    public int Index { get; private set; }

    public void SetItem(Item item, int index)
    {
        titleText.text = item.title;
        subtitleText.text = item.subtitle;
        Index = index;
    }
}

 

하나의 Cell 프리팹이 Item 데이터와 연결되도록 해주었다.

 

3. 오브젝트 풀

public class ObjectPool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] int poolSize;
    [SerializeField] private RectTransform parent;
    
    private Queue<GameObject> _pool;

    public GameObject GetObject()
    {
        if (_pool.Count == 0) CreateNewObject();
        GameObject dequeObject = _pool.Dequeue();
        dequeObject.SetActive(true);
        return dequeObject;
    }

    public void ReturnObject(GameObject returnObject)
    {
        returnObject.SetActive(false);
        _pool.Enqueue(returnObject);
    }
}

 

Instantiate / Destroy 반복을 피해 성능이 안정적으로 유지될 수 있도록 오브젝트 풀을 사용해주었다.

풀이 없으면 새로운 풀을 생성하고 사용이 완료되면 다시 비활성화하여 큐에 넣어주었다.

 

4. 핵심 로직

private void ReloadData()
{
    _visibleCells = new LinkedList<Cell>();

    // Content 크기 조정
    var contentSizeDelta = _scrollRect.content.sizeDelta;
    contentSizeDelta.y = _items.Count * cellHeight;
    _scrollRect.content.sizeDelta = contentSizeDelta;

    // 처음 보이는 Cell들만 추가
    var (startIndex, endIndex) = GetVisibleIndexRange();
    for (int i = startIndex; i < endIndex; i++)
    {
        var cellObject = ObjectPool.Instance.GetObject();
        var cell = cellObject.GetComponent<Cell>();
        cell.SetItem(_items[i], i);
        cell.transform.localPosition = new Vector3(0, -i * cellHeight, 0);

        _visibleCells.AddLast(cell);
    }
}

public void OnValueChanged(Vector2 value)
{
    if (_lastYValue < value.y)
    {
        ////////////////////////////////////////
        // 위로 스크롤

        // 1. 상단에 새로운 셀이 필요한지 확인 후 필요하면 추가
        var firstCell = _visibleCells.First.Value;
        var newFirstIndex = firstCell.Index - 1;

        if (IsVisibleIndex(newFirstIndex))
        {
            var cell = ObjectPool.Instance.GetObject().GetComponent<Cell>();
            cell.SetItem(_items[newFirstIndex], newFirstIndex);
            cell.transform.localPosition = new Vector3(0, -newFirstIndex * cellHeight, 0);
            _visibleCells.AddFirst(cell);
        }

        // 2. 하단에 있는 셀이 화면에서 벗어나면 제거
        var lastCell = _visibleCells.Last.Value;

        if (!IsVisibleIndex(lastCell.Index))
        {
            ObjectPool.Instance.ReturnObject(lastCell.gameObject);
            _visibleCells.RemoveLast();
        }
    }
    else
    {
        ////////////////////////////////////////
        // 아래로 스크롤

        // 1. 하단에 새로운 셀이 필요한지 확인 후 필요하면 추가
        var lastCell = _visibleCells.Last.Value;
        var newLastIndex = lastCell.Index + 1;
        
        if (IsVisibleIndex(newLastIndex))
        {
            var cell = ObjectPool.Instance.GetObject().GetComponent<Cell>();
            cell.SetItem(_items[newLastIndex], newLastIndex);
            cell.transform.localPosition = new Vector3(0, -newLastIndex * cellHeight, 0);
            _visibleCells.AddLast(cell);
        }

        // 2. 상단에 있는 셀이 화면에서 벗어나면 제거
        var firstCell = _visibleCells.First.Value;

        if (!IsVisibleIndex(firstCell.Index))
        {
            ObjectPool.Instance.ReturnObject(firstCell.gameObject);
            _visibleCells.RemoveFirst();
        }
    }

    _lastYValue = value.y;
}

 

스크롤 뷰의 Content 높이를 데이터 개수 만큼 조정하고

스크롤뷰 이벤트를 감지해서 보이는 셀만 유지되도록 하는 스크립트이다.

 

 

 

2. 안드로이드 빌드

1. 기본 개념

  • Unity와 그래픽 API
    • Unity는 각 운영체제(OS)에 맞는 그래픽 API를 자동 적용
      • Windows → DirectX
      • MacOS → Metal
      • Android → OpenGLES(2.x/3.x), 최근엔 Vulkan
  • 텍스처 압축
    • OpenGLES 2.x → ETC
    • OpenGLES 3.x → ETC2
    • 압축 방식에 따라 결과물 퀄리티가 달라짐

 

2. 빌드 환경 & 스크립팅 백엔드

  • Mono & C# 실행
    • Unity는 오픈소스 Mono Project를 기반으로 C# 코드를 실행 및 빌드
  • .NET 빌드 구조
    • C# → 중간언어(IL) 생성 → OS에 맞게 실행파일 변환
  • IL2CPP
    • Apple 정책상 iOS는 반드시 64bit 빌드 필요
    • Unity는 C# → C++ 변환 후 64bit로 빌드 (IL2CPP)
  • 모바일 필수 체크
    • Scripting Backend: IL2CPP
    • Target Architectures: ARM64
    • 위 옵션이 켜져 있어야 Google Play 업로드 가능

 

3. 안드로이드 빌드 옵션

  • Android Studio
    • 선택사항 (Unity만으로도 APK 빌드 가능)
    • 설치 시 Standard 옵션 + Empty Activity 기본 프로젝트
  • Export Project
    • APK 전단계 프로젝트를 생성 → Android Studio에서 직접 빌드 가능
    • 주로 센서(GPS, 가속도계 등)와 연동할 때 활용
  • Build App Bundle (Google Play)
    • .aab 파일 생성
    • Google Play 업로드는 .aab 형식 필수
  • Keystore
    • 앱 서명 파일. Google Play 업로드에 반드시 필요

 

4. 성능 분석 & 최적화

  • Profiler
    • CPU/메모리/객체 생성-소멸 모니터링
    • Window > Analysis > Profiler
    • Autoconnect Profiler + Development Build 체크 시 기기에서 자동 연결
    • Deep Profiling Support → 더 세밀한 추적 가능
  • Device Simulator
    • 다양한 기기 화면 비율/성능을 시뮬레이션
    • Package Manager > Unity Registry > Device Simulator 설치 후
      Window > General > Device Simulator 사용
  • 카메라 화면 비율 조정
    • Camera.aspect = 화면의 가로/세로 비율
    • orthographicSize = 카메라 세로 크기의 절반
    • → 기기에 맞게 카메라 크기를 자동 조정 가능
    • 예: widthUnit = 6 (틱택토 크기 기준)
using UnityEngine;

[RequireComponent (typeof(Camera))]
public class CameraController : MonoBehaviour
{
    [SerializeField] private float widthUnit;

    private Camera _camera;

    private void Start()
    {
        _camera = GetComponent<Camera> ();
        _camera.orthographicSize = widthUnit / _camera.aspect / 2;
    }
}

 

5. 빌드 및 테스트

(1) APK 빌드 & 설치

  1. 테스트용 폰 → 개발자 모드 활성화, USB 디버깅 ON
  2. Unity에서 Build & Run → APK 설치
  3. 필요 시 .aab 빌드 (Google Play 업로드용)

(2) USB 연결 테스트 (APK 없이)

  1. Android Studio에서 Empty Activity 프로젝트 생성
  2. 터미널에서 adb devices 입력
    • unauthorized → USB 디버깅 승인 필요
    • device 표시 → 연결 완료
  3. Unity Build & Run에서 연결된 기기 선택

(3) 간편 테스트 도구

  • Unity Remote 5
    • 모바일 화면을 바로 Unity 에디터와 연결해 테스트 가능
    • 단, 성능은 실제 APK 빌드와 다를 수 있음
  • 안드로이드 미러링 (scrcpy)