[Unity] GridLayoutGroup에서 마지막 줄이 가운데 정렬되지 않을 때 해결 방법 (트러블슈팅)
Unity로 UI 작업을 하다가 예상치 못한 정렬 문제를 겪었습니다.
바로 GridLayoutGroup을 사용했을 때, 마지막 줄의 아이템들이 자동으로 중앙 정렬되지 않는 문제였습니다.
처음에는 제 설정이 잘못된 줄 알았는데, 조사해보니 GridLayoutGroup 자체가 마지막 줄에 대한 정렬 보정 기능을 제공하지 않는 구조라는 걸 알게 되었습니다.
그래서 이 문제를 직접 해결하기 위해, 간단한 커스텀 레이아웃 스크립트를 작성하게 되었습니다.
문제 상황
예를 들어 columnCount = 3으로 설정하고 10개의 아이템을 배치하면 아래와 같은 형태가 됩니다.

하지만 기본 GridLayoutGroup은 마지막 줄(여기선 아이템 1개)을 왼쪽 정렬해버립니다.
아이템 수에 따라 마지막 줄이 1개, 2개처럼 애매하게 남을 경우 정렬이 어색하게 보이는 문제가 발생합니다.
시도했던 해결 방법
처음에는 Child Alignment 옵션을 바꿔보거나, 콘텐츠 사이즈 피터를 수정해보기도 했습니다.
하지만 정렬 기준은 전체 영역 기준이고, 마지막 줄만 따로 정렬하는 기능은 지원되지 않았습니다.
StackOverflow나 공식 포럼도 찾아봤지만, 명확한 해결책보다는 "직접 커스텀해야 된다"라는 식의 조언이 많았습니다.
해결 방법: 직접 커스텀 레이아웃 구현
결국 GridLayoutGroup을 쓰지 않고, RectTransform을 직접 계산해서 정렬하는 스크립트를 만들었습니다.
구현 방향은 다음과 같습니다:
- columnCount 기준으로 위치를 수동 계산
- 마지막 줄인지 판단 후, 그 줄만 가운데 정렬
- 전체 정렬 방식은 상단 기준 또는 중앙 기준 선택 가능
주요 기능
- 열 개수(columnCount) 고정
- 셀 크기, 간격 설정 가능
- 마지막 줄만 자동 가운데 정렬
- 상단 정렬 / 중앙 정렬 선택 지원 (isMiddleAlign 설정)
- 에디터에서도 실시간 반영 ([ExecuteAlways] + OnValidate)
적용 결과
적용 전 | 적용 후 | |
![]() |
![]() |
![]() |
코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// GridLayoutGroup처럼 자식 RectTransform을 그리드 형태로 정렬하는 커스텀 레이아웃 컴포넌트입니다.
/// 자식 오브젝트는 Pivot이 (0.5, 0.5)로 고정되어 있어야 하며,
/// 마지막 줄은 자식 수에 맞춰 중앙 정렬됩니다.
/// </summary>
[ExecuteAlways] // 에디터에서도 동작하게 만듭니다.
public class CustomGridLayout : MonoBehaviour
{
public Vector2 cellSize = new Vector2(100, 100); // 각 셀의 너비와 높이
public Vector2 spacing = new Vector2(10, 10); // 셀 사이의 간격
public int columnCount = 3; // 고정 열 개수
public bool isMiddleAlign = false; // true면 세로 중앙 정렬, false면 상단 정렬
private RectTransform rectTransform;
/// <summary>
/// 오브젝트가 활성화될 때 자동으로 레이아웃을 갱신합니다.
/// </summary>
private void OnEnable()
{
rectTransform = GetComponent<RectTransform>();
UpdateLayout();
}
/// <summary>
/// 자식 Transform이 추가/삭제되면 레이아웃을 자동 갱신합니다.
/// </summary>
private void OnTransformChildrenChanged()
{
UpdateLayout();
}
/// <summary>
/// 인스펙터에서 속성을 변경했을 때 자동 갱신합니다 (에디터 전용).
/// </summary>
private void OnValidate()
{
UpdateLayout();
}
/// <summary>
/// 자식들을 그리드 형태로 정렬합니다.
/// </summary>
public void UpdateLayout()
{
if (rectTransform == null)
rectTransform = GetComponent<RectTransform>();
int childCount = rectTransform.childCount;
// 활성화된 자식만 필터링합니다.
var activeChildren = new System.Collections.Generic.List<RectTransform>();
for (int i = 0; i < childCount; i++)
{
var child = rectTransform.GetChild(i) as RectTransform;
if (child != null && child.gameObject.activeSelf)
activeChildren.Add(child);
}
int actualCount = activeChildren.Count;
if (actualCount == 0) return;
// 전체 줄 수 계산
int rowCount = Mathf.CeilToInt((float)actualCount / columnCount);
// 전체 높이 계산 (spacing 포함)
float totalHeight = (cellSize.y + spacing.y) * rowCount - spacing.y;
Vector2 parentSize = rectTransform.rect.size;
// Y축 기준 offset (세로 정렬: 상단 또는 중앙)
float baseOffsetY = isMiddleAlign
? -(parentSize.y - totalHeight) / 2f // MiddleCenter 정렬
: 0f; // UpperCenter 정렬
// 자식 오브젝트마다 위치 계산
for (int i = 0; i < actualCount; i++)
{
RectTransform child = activeChildren[i];
int row = i / columnCount; // 몇 번째 줄
int col = i % columnCount; // 몇 번째 열
// 마지막 줄 여부 판별
bool isLastRow = row == rowCount - 1;
// 마지막 줄의 아이템 개수 (중앙 정렬 계산에 필요)
int itemsInLastRow = actualCount % columnCount;
if (itemsInLastRow == 0) itemsInLastRow = columnCount;
// 현재 줄의 실제 열 개수
int effectiveColCount = isLastRow ? itemsInLastRow : columnCount;
// 현재 줄의 전체 너비
float rowWidth = (cellSize.x + spacing.x) * effectiveColCount - spacing.x;
// X축 중앙 정렬 오프셋
float offsetX = (parentSize.x - rowWidth) / 2f;
// 중심 정렬 기준 위치 계산
float x = (cellSize.x + spacing.x) * col + offsetX + cellSize.x * 0.5f;
float y = -((cellSize.y + spacing.y) * row) + baseOffsetY - cellSize.y * 0.5f;
// 위치 및 크기 설정
child.anchorMin = new Vector2(0, 1); // 부모 좌상단 기준
child.anchorMax = new Vector2(0, 1);
child.pivot = new Vector2(0.5f, 0.5f); // 반드시 중앙 고정
child.anchoredPosition = new Vector2(x, y);
child.sizeDelta = cellSize;
}
}
}
사용 시 주의사항
- 자식 오브젝트의 Pivot은 반드시 (0.5, 0.5) 로 설정해야 위치가 정확하게 계산됩니다.
- RectTransform을 기준으로 하기 때문에 UI 요소 외 일반 GameObject에는 사용되지 않습니다.
- 런타임과 에디터 모두에서 동작하지만, 편의상 OnEnable, OnValidate, OnTransformChildrenChanged 이벤트에만 연동했습니다.
마무리
이번 문제를 겪으면서 Unity의 기본 UI 시스템이 의외로 단순한 구조라는 점을 느꼈습니다.
그동안은 기본 제공 컴포넌트만으로 대부분 해결될 거라 생각했지만, 실제로 사용해보니 세세한 부분에서는 손이 많이 가는 일이 많았습니다.
특히 마지막 줄만 중앙 정렬하는 기능처럼, 사소하지만 사용자 경험에 영향을 주는 요소는
직접 구현해보는 과정을 통해 Unity 내부 동작 방식을 조금씩 이해하게 되었습니다.
비록 코드가 완벽하진 않더라도, 문제를 정의하고 나만의 방식으로 해결해보는 경험이 저에게는 큰 공부가 되었습니다.