23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
안녕하세요, 저는 Comm Client 팀 이준영입니다. 저희 팀은 에이닷 앱 전화, 에이닷 전화(구. T전화) iOS 클라이언트 개발을 담당하고 있습니다.
iOS 앱을 개발하다 보면, 다양한 리소스 파일들을 다루게 됩니다. 이 중에서도 버튼, 아이콘처럼 여러 해상도와 테마에 대응해야 하는 이미지 리소스 관리는 매우 번거롭습니다.
그래서 오늘은 저희 팀이 이미지 리소스를 어떻게 관리하고 있는지에 대한 이야기를 공유해 드리려 합니다.
Xcode는 .xcassets 라는 Asset Catalog를 통해 각종 리소스를 관리합니다.
이 구조 덕분에 해상도(2배율, 3배율 대응)나 테마 모드(Dark, Light)등에 따라 여러 종류의 이미지를 제공하더라도, 코드에서는 아래와 같이 이름 하나만으로 이미지를 참조할 수 있습니다.
Image("some_button")만약 여러분이 some_button.png, some_button_dark.png 라는 이미지를 Xcode에 some_button으로 추가하면 내부적으로는 아래와 같은 폴더 구조가 생성됩니다.
Image.xcassets
└── some_button.imageset
├── some_button.png
├── some_button_dark.png
└── Contents.json여기서 .imageset 폴더는 하나의 "논리적 이미지" 단위를 나타내며, 실제 사용될 이미지 파일들과 Contents.json 파일로 구성됩니다.
각 .imageset 폴더 안에는 Contents.json이라는 파일이 항상 존재합니다.
이 파일은 해당 이미지 세트에 어떤 이미지 파일이 들어 있는지, 어떤 해상도와 테마에 대응하는지를 정의하는 메타 데이터를 담고 있습니다.
쉽게 말해, Xcode가 어떤 상황에서 어떤 이미지를 선택해야 하는지를 알려주는 역할을 합니다.
위 some_button의 예시를 보여드리겠습니다.
{
"images": [
{
"filename": "some_button.png",
"idiom": "universal",
"scale": "1x"
},
{
"appearances": [
{ "appearance": "luminosity", "value": "dark" }
],
"filename": "some_button_dark.png",
"idiom": "universal",
"scale": "1x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}이 구조를 구성하는 주요 요소들을 하나씩 살펴보면 다음과 같습니다.
images: 실제로 연결될 이미지 파일들의 집합입니다. 각 항목은 하나의 PNG 파일에 해당합니다.
filename
실제 사용될 PNG 파일 이름입니다.
scale
해당 이미지가 몇 배 해상도용인지 나타냅니다. (예: 1x, 2x, 3x)
idiom
어떤 플랫폼에서 사용할 수 있는지를 나타내며, 보통은 universal입니다.
appearances
해당 이미지가 어느 테마 모드에서 사용될지를 나타냅니다.
예를 들어, 다크모드일 경우 "appearance": "luminosity", "value": "dark"처럼 명시됩니다.
info: 이 Contents.json 파일 자체의 메타데이터입니다.
author
보통 xcode로 고정됩니다.
version
현재 버전은 1이며, Xcode 버전에 따라 내부 사용될 수 있는 값입니다.
결과적으로 위 예시는 라이트 모드에서는 some_button.png, 다크 모드에서는 some_button_dark.png를 자동으로 적용하도록 설정한 구조입니다.
(더 자세한 내용은 Apple 공식 문서에서 확인하실 수 있습니다)
이처럼 Contents.json 에는, 각 이미지가 어떤 조건에서 어떻게 사용할지 정의되어 있다는 것을 알 수 있습니다.
에이닷 내 전화의 경우, 아래와 같이 이미지 리소스를 구성하고 있습니다.
btn_call.imageset/
├── btn_call@2x.png
├── btn_call@3x.png
├── btn_call_dark@2x.png
├── btn_call_dark@3x.png
└── Contents.json보시다시피, 하나의 이미지 리소스가 총 4개의 이미지 파일로 구성되어 있습니다.
2배율 Light 모드 이미지
3배율 Light 모드 이미지
2배율 Dark 모드 이미지
3배율 Dark 모드 이미지
따라서 하나의 이미지 리소스를 변경하려면, 4개의 PNG 파일을 바꿔줘야 합니다.
25개의 리소스만 변경하려고 해도, 실제로는 100개의 이미지 파일을 교체해야 하는 것이죠.
이걸 사람이 하나하나 확인해 가면서 직접 옮기는 것은 너무 힘든 일입니다.
그래서 저희 팀은, 이미지 리소스 관리 자동화를 도입했습니다.
위에서 보셨듯이, 저희는 이미지 리소스를 일정한 규칙에 따라 관리하고 있습니다.
모든 파일 이름은 해상도와 테마 모드를 기준으로 이름 짓고 있으며, 아래와 같은 규칙을 따르고 있습니다.
Light 모드일 경우
{리소스 이름}@{해상도}.png
ex) btn_call@2x.png
Dark 모드일 경우
{리소스 이름}_dark@{해상도}.png
ex) btn_call_dark@2x.png
따라서, 위 규칙만 따른다면 아래와 같이 정보를 추출할 수 있습니다.
base_name
이미지의 논리적인 이름
appearance
테마모드
scale
해상도
그래서 다음과 같이 자동화 목표를 잡았습니다.
파일명에서 이미지 정보 추출
같은 base_name을 가진 이미지 파일들을 그룹화
Contents.json을 구성하기 위한 메타정보 생성
.imageset 생성 및 Contents.json 생성
이제 단계별로 실제 스크립트를 작성해 보겠습니다.
제일 먼저, 이미지 파일 이름으로부터 이미지 정보를 알아내야 합니다.
예를 들어 btn_call_dark@2x.png 라는 파일명이 주어지면 다음과 같은 정보를 알아낼 수 있습니다.
이미지 리소스 이름 : btn_call
해상도 : 2배율(2x)
테마 모드 : 다크모드
이 정보를 추출하는 코드는 아래와과 같습니다.
def extract_details(filename):
# 예: btn_call_dark@2x.png -> [btn_call_dark, 2x.png]
parts = filename.split('@')
base_name = parts[0]
# 이미지 해상도 정보 추출
scale = parts[1].split('.')[0] if len(parts) > 1 else '1x'
# 테마 정보 추출 (dark appearance)
appearance = 'dark' if 'dark' in base_name else None
# 실제 리소스 이름 정리: btn_call_dark -> btn_call
if appearance:
base_name = base_name.replace('_dark', '')
return base_name, scale, appearance
# 예시 실행 결과
extract_details('btn_call_dark@2x.png') # ('btn_call', '2x', 'dark')이제 extract_details() 함수를 통해 각 파일의 정보를 얻을 수 있게 되었으니, 본격적으로 같은 리소스 이름(base_name)을 가진 이미지 파일들을 그룹으로 묶는 작업을 해보겠습니다.
여기에서 목적은 같은 base_name을 가진 PNG 파일들(ex. btn_call@2x.png, btn_call_dark@3x.png) 같은 파일들을 하나의 .imageset으로 묶기 위한 준비 작업입니다.
아래는 디렉터리 전체를 재귀적으로 순회하면서 PNG 파일들을 그룹화하는 함수입니다.
def group_png_files(source_folder):
image_groups = {}
# 모든 파일을 재귀적으로 탐색하면서 그룹화
for root, _, files in os.walk(source_folder):
for png_file in files:
# PNG 파일인지 확인
if png_file.endswith('.png'):
full_path = os.path.join(root, png_file)
# 파일명에서 base_name, 해상도(@2x, @3x), 다크모드 여부 추출
base_name, scale, appearance = extract_details(png_file)
# 기준: base_name
key = base_name
# 동일 base_name의 이미지들을 하나의 리스트로 묶기
image_groups.setdefault(key, []).append(
(full_path, png_file, scale, appearance)
)
return image_groups이제 이 그룹을 기반으로 .imageset 폴더를 만들고, 각각의 이미지 파일을 복사한 뒤, 그에 맞는 Contents.json 파일을 생성하는 단계만 남았습니다.
앞서 설명해 드렸듯이, Xcode는 .imageset 폴더 내부에 있는 Contents.json 파일을 기준으로 어떤 이미지를 어떤 상황에서 사용할지 판단합니다.
이 파일에는 이미지의 해상도(scale), 테마 모드(appearance), 파일 이름(filename) 등의 메타 정보가 담깁니다.
예를 들어, btn_call_dark@2x.png 파일에 대한 Contents.json 항목은 다음과 같이 구성되어야 합니다.
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"filename": "btn_call_dark@2x.png",
"idiom": "universal",
"scale": "2x"
}이 내용을 생성하는 코드는 아래와 같습니다.
def create_image_info(filename, scale, appearance_value=None):
info = {
"idiom": 'universal', # 대부분의 경우 universal로 설정 (모든 디바이스에서 사용 가능)
"scale": scale, # 해상도: 1x, 2x, 3x 중 하나
"filename": filename # 실제 PNG 파일명
}
# 다크 모드용 이미지라면 appearance 정보 추가
if appearance_value:
info["appearances"] = [
{
"appearance": "luminosity", # 밝기 조건 기준
"value": appearance_value # "dark"일 경우 다크 모드용 이미지
}
]
return info이제 PNG 파일을 분석해서 이미지 이름(base_name)을 기준으로 묶었고, 각 파일에 대해 메타 정보도 생성할 수 있습니다.
이제 남은 작업은, 각 그룹을 .imageset 폴더로 변환하고, 해당 폴더에 이미지 파일을 복사한 뒤, 그에 맞는 Contents.json 파일을 생성하는 것입니다.
이걸 코드로 작성하면 아래와 같습니다.
def convert_image_groups_to_imageset(image_groups, destination_folder):
for base_name, items in image_groups.items():
# 1. .imageset 디렉토리 생성 (예: ./target/btn_call.imageset)
target_dir = os.path.join(destination_folder, f"{base_name}.imageset")
os.makedirs(target_dir, exist_ok=True)
images_json = []
# 2. 이미지 파일 복사 및 메타 정보 생성
for file_path, filename, scale, appearance in items:
# PNG 파일을 해당 imageset 폴더로 복사
shutil.copy2(file_path, os.path.join(target_dir, filename))
# 해당 PNG의 메타 정보 구성
images_json.append(create_image_info(filename, scale, appearance))
# 3. Contents.json 파일 생성
contents = {
"images": images_json,
"info": {
"author": "xcode", # Xcode가 기본적으로 요구하는 값
"version": 1 # 현재 사용되는 포맷 버전
}
}
with open(os.path.join(target_dir, "Contents.json"), "w", encoding="utf-8") as f:
json.dump(contents, f, indent=2)```이 함수를 실행하면, destination_folder 아래에 Xcode에서 바로 사용할 수 있는 폴더 구조로 자동으로 생성됩니다.
수많은 PNG 리소스를 개발자가 손댈 필요 없이 .xcassets에 바로 집어넣을 수 있는 상태로 만드는 것입니다.
자, 이제 png 파일들을 변환하는 데 필요한 코드는 모두 작성했습니다.
이제 실제로 자동화 스크립트를 실행해 보겠습니다.
image_groups = group_png_files(source_folder)
convert_image_groups_to_imageset(image_groups, destination_folder)```간단히 두 줄이면 끝입니다.
source_folder 안의 PNG 파일들을 전부 순회하면서 이미지 이름(base name)을 기준으로 그룹화하고,
destination_folder에는 Xcode가 바로 사용할 수 있는 .imageset 구조가 자동으로 생성됩니다.
Before | After |
|---|---|
이제 이 결과물을 Xcode 프로젝트의 .xcassets 디렉터리에 그대로 복사해 넣으면, 아래와 같이 이미지가 자동으로 적용됩니다.
이 스크립트를 도입한 이후, 이미지 리소스를 관리하는 방식이 완전히 달라졌습니다.
기존에는 디자이너로부터 수십, 때로는 수백 개에 달하는 PNG 파일을 받을 때마다 일일이 파일을 Drag&Drop해야 했습니다. 그 과정에서 실수도 종종 발생했고요.
하지만 이제는 몇 초 만에 모든 리소스를 자동으로 정리할 수 있게 되었고, 실수의 여지도 거의 사라졌습니다.
디자인이 자주 바뀌는 프로젝트일수록 이 자동화의 효과는 더욱 크며, 개발자는 반복 업무 대신 진짜 개발에 집중할 수 있게 됩니다.
혹시 여러분도 이미지 리소스 때문에 고생 중이라면, 이 방식으로 자동화를 시도해 보는 걸 강력히 추천해 드립니다!
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.