ImageMagick으로 앱 아이콘 만들기: SVG가 안 그려지고 마스크가 투명해진 이유
앱 하나를 두 스토어에 올리면서 이미지 21개를 만들어야 했습니다. 아이콘, 적응형 아이콘 레이어들, 스플래시, 파비콘, 그리고 꾸민 스토어 스크린샷 12장. 디자인 도구를 열지 않고 전부 명령줄에서 만들었습니다.
쓴 도구는 ImageMagick입니다. 이미 깔려 있었습니다.
Version: ImageMagick 7.1.2-3 Q16-HDRI aarch64간단히 말하면 이미지 편집을 명령어로 하는 도구입니다. 크기 조절, 자르기, 도형 그리기, 글자 얹기, 합성을 셸에서 합니다. 사람이 한 장 만드는 데는 GUI가 빠르지만, 같은 작업을 12장에 반복하거나 문구를 고쳐 전부 다시 뽑아야 할 때는 이쪽이 압도적으로 빠릅니다. 실제로 스크린샷 문구를 한 번 잘못 잡아 12장을 통째로 다시 만들었는데, 스크립트라 명령 한 줄로 끝났습니다.
버전 7부터 명령어가 magick 하나로 통일됐습니다. 인터넷에 돌아다니는 예제의 convert, composite, identify는 6 시절 이름이고 지금은 magick, magick composite, magick identify로 씁니다.
세 번 막혔고, 셋 다 에러 메시지가 친절하지 않았습니다.
SVG를 줬는데 배경만 그려졌습니다
아이콘을 SVG로 그려서 PNG로 뽑을 생각이었습니다. 십자가와 접힌 지도를 <path>로 정의하고 변환했더니 배경 사각형만 남고 나머지가 사라졌습니다. 에러는 없었습니다. 종료 코드도 0이었습니다.
델리게이트 목록을 보고 이유를 알았습니다.
$ magick -list delegate | grep svg
svg => "rsvg-convert' --dpi-x %x --dpi-y %y -o '%o' '%i"
$ command -v rsvg-convert
(없음)ImageMagick은 SVG를 직접 렌더링하지 않습니다. rsvg-convert라는 외부 프로그램에 넘깁니다. 그게 없으니 내장된 최소 파서로 떨어졌고, 그 파서는 단순한 사각형만 이해하고 path는 조용히 버립니다. 실패를 실패라고 말하지 않는 게 가장 나쁜 부분이었습니다.
brew install librsvg로 해결할 수 있었지만, 도형 몇 개짜리 아이콘 하나 때문에 의존성을 늘리고 싶지 않아서 SVG를 버리고 -draw로 직접 그렸습니다.
-draw는 별도의 언어입니다
접힌 지도 3면과 라틴 십자가를 좌표로 정의했습니다. 1024 캔버스 기준입니다.
P1="path 'M 192 504 L 396 436 L 396 824 L 192 892 Z'"
P2="path 'M 416 440 L 608 504 L 608 892 L 416 828 Z'"
P3="path 'M 628 504 L 832 436 L 832 824 L 628 892 Z'"
CV="rectangle 472,136 552,672"
CH="rectangle 352,264 672,344"
magick -size 1024x1024 "xc:#0F172A" \
-stroke none -fill '#F6E7C8' -draw "$P1" -draw "$P2" -draw "$P3" \
-fill '#E9B44C' -draw "$CV" -draw "$CH" \
icon.png여기서 두 번째로 막혔습니다. 선 끝을 둥글게 하려고 -linecap round를 붙였더니 인식하지 못한다고 나왔습니다. -strokelinecap도 마찬가지였습니다.
-draw 안쪽은 CLI 옵션과 다른 별개의 미니 언어입니다. 그리기 관련 설정은 옵션이 아니라 그리기 문자열 안에 써야 합니다.
-draw "stroke-linecap round bezier 10,90 40,10 60,90 90,10"fill, stroke, stroke-width, stroke-linecap, translate, rotate 전부 이 문자열 안에서 씁니다. SVG 속성 이름과 거의 같아서 익숙해지면 오히려 편합니다.
접힌 선을 그릴 때는 획을 긋는 대신 면 사이에 간격을 뒀습니다. 위 좌표에서 396과 416처럼 20px 벌어져 있는 부분입니다. 획으로 그으면 Android 13 테마 아이콘처럼 전체가 단색으로 칠해질 때 선이 사라져 형태를 알아볼 수 없게 됩니다. 구멍은 단색화돼도 남습니다.
마스크가 통째로 투명해졌습니다
제일 오래 붙잡은 문제입니다.
스토어 스크린샷을 꾸미면서 안쪽 캡처의 위 모서리만 둥글게 깎으려고 마스크를 만들었습니다.
magick -size "${SW}x${SH}" xc:black \
-draw "roundrectangle 0,0 $((SW-1)),$((SH+CORNER)) $CORNER,$CORNER" \
mask.png
magick shot.png mask.png -alpha off -compose CopyOpacity -composite rounded.png결과물의 스크린샷 자리가 텅 비었습니다. 배경과 헤드라인은 멀쩡한데 그 아래가 아무것도 없었습니다. 좌표를 의심해서 한참 헤맸는데 좌표는 맞았습니다.
CopyOpacity는 마스크의 밝기를 알파 값으로 읽습니다. 흰 부분은 불투명, 검은 부분은 투명입니다. 그런데 -draw의 기본 fill은 검정입니다. 검정 캔버스에 검정 둥근 사각형을 그린 셈이라 전부 밝기 0, 즉 전부 투명이었습니다.
한 마디를 넣어 고쳤습니다.
magick -size "${SW}x${SH}" xc:black -fill white \
-draw "roundrectangle ..." mask.png마스크를 만들 때는 -fill white를 반드시 적는다고 외우는 게 낫습니다. 이것도 에러가 나지 않습니다. 그냥 조용히 빈 이미지가 나옵니다.
알아두니 쓸모 있던 것들
한쪽 치수만 지정하기. -resize "780x"는 폭만 780으로 맞추고 높이는 비율대로 따라옵니다. x780이면 반대입니다.
그림자. 이 조합을 통째로 외워두면 됩니다.
\( image.png \( +clone -background black -shadow 34x28+0+10 \) +swap -background none -layers merge +repage \)+clone으로 복제하고, 그 복제본을 그림자로 흐리고, +swap으로 앞뒤를 바꿔 원본이 위로 오게 한 다음 합칩니다. 순서를 바꾸면 그림자가 이미지를 덮습니다.
글자는 두 가지 방법이 있습니다. label:"텍스트"는 글자 크기에 딱 맞는 새 이미지를 만들고, -annotate는 기존 이미지 위에 얹습니다. 뱃지 칩처럼 글자 폭을 재서 배경 알약을 그려야 할 때는 label:로 만든 뒤 magick identify -format '%w'로 폭을 읽는 방식이 편했습니다.
한글 폰트. macOS에 기본으로 있는 /System/Library/Fonts/AppleSDGothicNeo.ttc를 지정하면 됩니다. 따로 설치할 게 없습니다.
중간 확인. magick identify -format '%f %wx%h %[size]\n' *.png 한 줄이면 만들어진 파일 목록과 크기를 한눈에 봅니다. 스토어 규격에 맞는지 확인할 때 매번 썼습니다.
다시 한다면
SVG로 시작하려면 magick -list delegate | grep svg로 렌더러가 실제로 있는지부터 확인하겠습니다. 없다면 librsvg를 깔거나 처음부터 -draw로 가는 게 낫습니다. 어중간하게 시작했다가 조용한 실패를 디버깅하는 게 제일 손해였습니다.
그리고 조용한 실패가 이 도구의 성격입니다. 세 번 막혔는데 셋 다 에러 메시지가 없었고, 결과 이미지를 눈으로 보고서야 알았습니다. 스크립트를 짤 때 단계별 중간 결과를 파일로 남겨두면 어디서 틀어졌는지 찾기가 훨씬 쉽습니다. 저도 마스크 문제를 잡을 때 mask.png를 열어보고 나서야 원인을 알았습니다.
우리 서비스에 적용할 계획이 있으신가요?
모바일 앱 개발 살펴보기 →함께 읽기
- Liquid Glass 아이콘 직접 만들기: .icon 파일 구조와 librsvg 함정앱 아이콘 하나 만드는 데 하루를 썼습니다. 열 번 넘게 갈아엎었고 빌드를 네 번 중간에 끊었습니다. 그 과정에서 알아낸 것 중에 검색해도 잘 안 나오는 게 두 개 있어서 적어둡니다.
- 스토어 이미지 규격 정리: 스크린샷·아이콘·그래픽에 실제로 필요한 것앱 하나를 App Store와 Google Play에 동시에 올리면서 이미지를 몇 개나 만들었는지 세어봤습니다. 스크린샷 12장, 스토어 전용 그래픽 2개, 앱 번들에 들어가는 아이콘 자산 7개. 합쳐서 21개입니다. 코드를 다 짜놓고 시작한 마무리 작업인데 결국 여기에 시간을 더 썼습니다.
- 환경변수로 둔 CLI 기본값: 비대화형 셸에서 사라지는 이유문서를 저장하는 CLI를 하나 만들었습니다. 저장할 때마다 그 문서가 어디로 떨어졌는지 찾는 일이 반복돼서, 새 문서는 인박스로 가도록 기본값을 정했습니다. 터미널에서 쳐 보니 의도한 자리에 들어갔습니다.
- Leaflet 지도가 느려지는 3가지 원인: React key, SVG 렌더러, 가드 위치지도 위에 H3 육각 격자를 얹은 화면을 만들었습니다. 셀을 누르면 인접한 여섯 셀이 함께 칠해지는, 영업 구역을 나눠 보는 화면입니다.
- FVM으로 Flutter 버전 관리하기: 프로젝트별 SDK 고정부터 CI까지Flutter 프로젝트가 하나뿐일 때는 시스템에 설치된 SDK 하나로도 충분해 보입니다. 하지만 유지보수 중인 앱, 신규 앱, 검증용 브랜치가 서로 다른 Flutter 버전을 요구하기 시작하면 전역 SDK를 계속 바꾸는 방식은 금방 불편해집니다.