스티커를 내보낸 뒤 검은 테두리가 생긴다면, PNG 파일이 손상된 것이 아니라 미리 곱한 색상 값을 스트레이트 색상 값으로 처리했거나 그리기 단계에서 불투명 배경을 먼저 채운 경우가 많습니다. 클라우드 Mac에서 검증할 때는 먼저 픽셀 세 개로 인코딩 규약을 확정한 다음 실제로 내보낸 파일을 검사하세요. 처음부터 전체 스크린샷을 비교하면 크기 조절, 표시 배경, 색상 변환의 영향이 한꺼번에 섞여 원인을 판단하기 어려워집니다.
먼저 픽셀 규약 확정하기
이 글의 검증 대상은 SDR, 8비트, sRGB PNG이며, 광색역과 HDR은 다루지 않습니다. 완전 투명, 반투명 빨강, 완전 불투명 파랑으로 이루어진 픽셀 세 개를 한 줄로 준비합니다. 출력 크기는 반드시 3×1을 유지해야 하며, 인코딩 후 다시 읽는 과정에서 투명도가 사라져서는 안 됩니다.
표현 방식은 두 계층으로 나누어 봐야 합니다. PNG는 알파를 미리 곱하지 않은 표현을 사용하지만, CoreGraphics 비트맵 컨텍스트는 미리 곱한 표현을 사용할 수 있습니다. 미리 곱한 RGBA 형식으로 명시한 버퍼에서 반투명 빨강의 빨강 채널과 알파 값은 모두 약 128입니다. 빨강 채널 값이 255인 것이 아닙니다.
CGImage의 dataProvider 바이트를 곧바로 표준화된 RGBA 데이터로 취급하지 마세요. 색 공간, 채널 순서, 행 간격이 모두 다를 수 있습니다. 직접 정의한 비트맵 컨텍스트에 먼저 그린 다음 바이트를 읽으세요.
완전 투명 픽셀의 RGB 값은 합격 기준에 포함하지 않습니다. 이 색은 화면에 보이지 않으며, 인코딩과 변환을 거치면 원래 값이 유지되지 않을 수도 있습니다. 알파가 0인지, 합성 후 배경이 바뀌지 않는지만 확인합니다.
Swift로 최소 왕복 검증 코드 만들기
전체 Xcode가 설치된 노드에서 다음 코드를 alpha-check.swift로 저장하세요. 시스템 프레임워크만 사용하며, PNG를 생성한 뒤 다시 디코딩합니다. 바이트 순서, 색 공간, 알파를 미리 곱하는 형식도 명시적으로 지정합니다. 출력은 현재 디렉터리에 저장되므로, 실행 전에 별도의 테스트 디렉터리로 이동하세요.
import Foundation
import CoreGraphics
import ImageIO
import UniformTypeIdentifiers
let width = 3
let height = 1
let space = CGColorSpace(name: CGColorSpace.sRGB)!
let info = CGBitmapInfo.byteOrder32Big.rawValue
| CGImageAlphaInfo.premultipliedLast.rawValue
func context() -> CGContext {
CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: space,
bitmapInfo: info
)!
}
let source = context()
source.clear(CGRect(x: 0, y: 0, width: width, height: height))
source.setFillColor(red: 1, green: 0, blue: 0, alpha: 0.5)
source.fill(CGRect(x: 1, y: 0, width: 1, height: 1))
source.setFillColor(red: 0, green: 0, blue: 1, alpha: 1)
source.fill(CGRect(x: 2, y: 0, width: 1, height: 1))
let url = URL(fileURLWithPath: "alpha-fixture.png")
let destination = CGImageDestinationCreateWithURL(
url as CFURL, UTType.png.identifier as CFString, 1, nil
)!
CGImageDestinationAddImage(destination, source.makeImage()!, nil)
precondition(CGImageDestinationFinalize(destination), "PNG write failed")
let reader = CGImageSourceCreateWithURL(url as CFURL, nil)!
let image = CGImageSourceCreateImageAtIndex(reader, 0, nil)!
precondition(image.width == width && image.height == height)
func pixels(background: CGFloat?) -> [UInt8] {
let target = context()
let bounds = CGRect(x: 0, y: 0, width: width, height: height)
target.clear(bounds)
if let value = background {
target.setFillColor(
red: value, green: value, blue: value, alpha: 1
)
target.fill(bounds)
}
target.interpolationQuality = .none
target.draw(image, in: bounds)
return Array(UnsafeBufferPointer(
start: target.data!.assumingMemoryBound(to: UInt8.self),
count: width * 4
))
}
func near(_ actual: UInt8, _ expected: Int) -> Bool {
abs(Int(actual) - expected) <= 2
}
let rgba = pixels(background: nil)
precondition(rgba[3] == 0)
precondition(near(rgba[4], 128) && near(rgba[7], 128))
precondition(rgba[8] == 0 && rgba[10] == 255 && rgba[11] == 255)
let black = pixels(background: 0)
let white = pixels(background: 1)
precondition(near(black[4], 128) && black[5] == 0 && black[6] == 0)
precondition(white[4] == 255 && near(white[5], 127))
precondition(near(white[6], 127))
precondition(Array(white[0...3]) == [255, 255, 255, 255])
precondition(Array(black[0...3]) == [0, 0, 0, 255])
실행할 때는 종료 코드를 확인할 수 있도록 유지하고, 이미지 뷰어의 배경색으로 성공 여부를 판단하지 마세요.
mkdir -p alpha-check
cd alpha-check
xcrun swift ../alpha-check.swift
검증 조건을 충족하지 못하면 0이 아닌 종료 코드로 종료됩니다. 이 검증 스크립트에는 어설션 검사를 제거하는 최적화 옵션을 사용하지 마세요. 허용 오차 2는 8비트 양자화 오차를 수용하기 위한 것이지, 오차 범위를 임의로 늘려도 된다는 뜻이 아닙니다.
검정·흰색 배경으로 두 가지 오류 구분하기
위 비트맵 컨텍스트에서 반투명 빨강을 검정 배경에 합성한 결과는 약 (128,0,0), 흰색 배경에 합성한 결과는 약 (255,127,127)이어야 합니다. 이는 해당 컨텍스트의 합성 결과를 규정한 것이며, 모든 렌더러에서 같은 색이 나온다는 보편적인 보장은 아닙니다.
검정 배경만 검사하면 일부 문제를 놓칠 수 있습니다. 예를 들어 내보내기 전에 이미 검정 배경을 채웠다면, 파일에서 빨강은 여전히 보이지만 투명 픽셀은 불투명한 검정 픽셀로 바뀝니다. 흰색 배경 검사는 이런 배경 오염을 즉시 드러냅니다.
이미 알파를 곱한 빨강에 알파를 한 번 더 곱하면 색이 어두워져 반투명 윤곽에 검은 테두리가 생기기 쉽습니다. 반대로 스트레이트 색상 값을 미리 곱한 색상 값으로 잘못 처리하면 가장자리가 지나치게 밝아질 수 있습니다. 문제를 추적할 때는 버퍼에 선언된 형식을 먼저 확인하고 변환이 이루어지는 지점을 살펴보세요. 선명도를 높여 해결하려 해서는 안 됩니다.
실제 iOS 내보내기 경로에 테스트 픽스처 연결하기
위 스크립트는 도구와 검증 조건이 신뢰할 만한지 확인할 뿐, 앱의 내보내기 로직이 검증을 통과했다는 뜻은 아닙니다. 다음 단계에서는 동일한 세 픽셀 테스트 픽스처를 실제 서비스에서 사용하는 인코딩 함수에 입력하세요. 예제 인코더를 다시 호출하는 대신, 해당 함수가 반환한 데이터를 다시 읽어 검사해야 합니다.
UIKit 그리기는 별도로 검증하기
앱에서 UIGraphicsImageRenderer를 사용한다면 iOS 테스트 타깃에서 format.opaque = false를 명시적으로 설정하세요. 픽셀 크기를 검사하는 테스트에서는 format.scale = 1도 설정해야 논리적 크기와 파일의 픽셀 크기를 혼동하지 않습니다. 실제 서비스에서 다른 배율을 사용한다면 해당 크기를 확인하는 검증 조건을 별도로 추가하세요.
반환된 PNG 데이터를 ImageIO로 디코딩한 뒤, 앞서 사용한 것과 동일한 sRGB 컨텍스트에 그려 표현을 통일합니다. UIImage.size를 출력 픽셀 수로 간주해서는 안 됩니다. 메인 스레드에서 그리는 작업이 포함된 테스트는 메인 액터에서 실행해 테스트 자체가 불확정적인 동작을 유발하지 않도록 하세요.
안티앨리어싱이 적용된 사선 가장자리 테스트 이미지도 하나 추가하세요. 알파 값이 0과 최댓값 사이인 가장자리 픽셀이 실제로 존재하는지 확인한 다음, 검정 배경과 흰색 배경에 각각 합성합니다. 특정 시스템 버전에서 나온 가장자리의 모든 바이트 값을 고정된 정답으로 삼지는 마세요.
실패 원인을 추적할 수 있는 증거 남기기
검증에 실패할 때마다 최소한 원본 PNG, 디코딩한 이미지 크기, 지정한 색 공간, 샘플링 좌표, 예상 RGBA와 실제 RGBA, 툴체인 버전을 저장하세요. 테스트용으로 생성한 이미지만 있으면 되므로 사용자 이미지를 업로드할 필요는 없습니다.
크기는 맞지만 모든 픽셀이 불투명하다면 먼저 그리기 컨텍스트의 opaque 설정과 배경 채우기를 확인하세요. 알파는 올바른데 가장자리가 어두워진다면 알파를 중복으로 곱했는지 확인하세요. 전체적으로 색이 달라졌다면 픽셀 허용 오차를 논하기 전에 색 공간 변환부터 점검하세요.
최종 통과 기준은 크기 일치, 투명도 유지, 규약에 맞는 배경 합성이라는 세 가지 독립된 결과로 나눕니다. 파일이 열린다는 것은 선행 조건일 뿐입니다. 공통 픽셀 처리 로직은 클라우드 Mac에서 자동으로 실행할 수 있지만, 실제 UIKit 내보내기 결과와 공유 후 최종 파일은 별도로 검증해야 중간 단계의 재인코딩 문제를 막을 수 있습니다.
자주 묻는 질문
알파 채널이 있으면 투명도가 보존된 것인가요?
아닙니다. 채널의 모든 값이 불투명할 수도 있습니다. 알려진 투명 및 반투명 픽셀을 다시 읽어 알파 값과 배경 합성 결과를 확인해야 합니다.
Mac 픽셀 테스트로 iOS 기기 검증을 대체할 수 있나요?
완전히 대체할 수는 없습니다. 공통 인코딩 로직과 파일 픽셀은 검증할 수 있지만 UIKit 렌더링, 디스플레이 색역, 실제 공유 경로는 iOS 테스트가 필요합니다.
다음 빌드를 위한 클라우드 Mac을 준비하세요
가상 머신이 아닌 전용 물리 Mac mini입니다. 일·주·월·분기 단위로 대여 기간을 선택할 수 있으며, 실제 이용 가능 여부와 구성은 콘솔에서 확인하세요.