이번에 팀 개편이 이뤄지면서 팀별 컨벤션 그리고 디자이너팀과 프런트 엔드팀이 컨벤션을 논의하게 되었다. 그중에 기존에는 ttf를 사용했으나 ttf를 사용할 때 디자이너들이 원하는 이미지가 제대로 나오지 않거나 만드는데 많은 시간이 걸려 비효율적이라서 ttf 대신 svg을 사용하고 싶다는 디자이너 팀의 의견을 듣게 되었다. 이를 수용해서 이번에 앱에 SVG를 사용할 수 있도록 세팅하고 SVG 파일들을 Dropbox에서 다운로드하고 그 토대로 type을 정의하는 자동화 작업을 진행하게 되었다.
자동화 작업을 하기 전에 규칙이 하나 있다. 우선 수동으로 기능이 작동하게끔 완성을 한 후에 자동화를 작업할 것. 당연한 생각일 수도 있지만 손으로 우선 된다면 기능의 문제는 없고 자동화는 언제든지 작업을 하면 되기 때문에 1단계는 수동 2단계를 자동화 작업으로 생각해서 작업을 분리해서 진행하는 게 나는 중요하다고 생각한다. 이것 또한 팀장님에게 배운 점!
Dropbox를 통해 앱에 사용될 SVG 이미지를 공유받기로 해서 해당 Dropbox 폴더에서 읽기 전용 링크를 생성한 다음 링크를 디자인팀에 공유해 주고 링크의 맨 끝 부분이 dl=0으로 되어 있을 텐데 dl=1로 바꿔서 사용하면 zip 파일을 다운로드하는 링크가 된다.
해당 링크를 활용해 Dropbox에 업로드된 SVG 이미지를 다운로드하고, 각 이미지의 파일명을 기반으로 type을 정의한 TypeScript 파일을 생성하는 JavaScript 코드를 작성하였다. 나는 이 작업을 위해 curl 명령어를 사용하였다.
SVG 파일들이 들어 있는 Zip 파일을 Dropbox로부터 다운로드한 후 unzip 해서 내가 원하는 폴더에 SVG 파일들을 풀어주고 Zip 파일은 삭제한다.
const fs = require('fs');
const path = require('path');
const util = require('util');
const childProcess = require('child_process');
const execFile = util.promisify(childProcess.execFile);
const localFilePath = path.join(__dirname, 'downloaded.zip');
const targetDirectoryPath = path.join(__dirname, 'new');
const outputFile = path.resolve(__dirname, 'src', 'iconImages.ts');
const url = 'DropboxURL?dl=1';
async function downloadImages() {
await execFile('curl', ['-L', '-o', localFilePath, url]);
await execFile('unzip', [localFilePath, '-x', '/', '-d', targetDirectoryPath]);
await execFile('rm', ['-rf', localFilePath]);
}
downloadImages();
SVG 파일들을 readdirSync 함수로 읽어오고 writeFileSync 함수로 각각의 파일명을 key로 정의해서 ts 파일을 생성한다.
const picturesEntries = fs
.readdirSync(targetDirectoryPath)
.filter(fileName => {
const extname = path.extname(fileName);
if (!(extname === '.svg')) {
return false;
}
return true;
})
.map(fileName => {
const key = path.basename(fileName, path.extname(fileName));
return [key, fileName];
});
const picturesKeys = picturesEntries.map(v => `'${v[0]}'`).join(' |\n ');
const picturesDictString = picturesEntries
.map(v => {
return ` '${v[0]}': require('@assets/iconImages/${v[1]}'),`;
})
.join('\n');
fs.writeFileSync(
outputFile,
`import type {ImageRequireSource} from 'react-native';
export type MyAppIconImagesKeyType = ${picturesKeys};
export const myAppIconImages: Readonly<Record<MyAppIconImagesKeyType, ImageRequireSource>> = {
${picturesDictString}
} as const;`,
);
이렇게 작업을 하면 다음과 같은 ts 파일이 생성된다.
export type AclosetIconImagesKeyType = 'myAppLayout' |
'myAppLogo-single' |
'myAppLogo' |
'adjust-fill' |
'adjust-stroke'
export const myAppIconImages: Readonly<Record<MyAppIconImagesKeyType, ImageRequireSource>> = {
'myAppLayout': require('@assets/iconImages/myAppLayout.svg'),
'myAppLogo-single': require('@assets/iconImages/myAppLogo-single.svg'),
'myAppLogo': require('@assets/iconImages/myAppLogo.svg'),
'adjust-fill': require('@assets/iconImages/adjust-fill.svg'),
'adjust-stroke': require('@assets/iconImages/adjust-stroke.svg')
}
SVG 파일들을 다운로드하고 그 파일들의 타입을 정의하는 작업까지 하였다. 이제 이 SVG 파일들을 사용하기만 하면 된다.
평소 Collapsible을 구현하기 위해 항상 react-native-collapsible 라이브러리를 사용해 왔는데, 최근에 그 라이브러리의 저자가 만든 react-native-vector-image 라이브러리를 발견했다. 이 라이브러리는 react-native에서 SVG를 사용할 때 활용되는데, 저자에 따르면 render 속도가 react-native-svg보다 5배 이상 빠르다고 한다. 또한 JS 번들 크기가 작아서 앱의 startup도 빠르고, 라이브러리 자체도 사용하기 간편하다고 생각이 들었다. 그래서 react-native-vector-image 라이브러리를 사용해 보기로 했다. 이 라이브러리를 사용하면 SVG 이미지를 더 효율적으로 활용할 수 있을 것 같았다.
yarn add react-native-vector-image @klarna/react-native-vector-drawable
android/app/build.gradle 파일에
project.ext.react = [
enableHermes: false, // clean and rebuild if changing
]
apply from: "../../node_modules/react-native/react.gradle"
+ apply from: "../../node_modules/react-native-vector-image/strip_svgs.gradle"
+를 뺀
apply from: "../../node_modules/react-native-vector-image/strip_svgs.gradle"
이 부분만 추가한다.
Xcode안에서 Build Phases 탭을 선택한 다음 Bundle React Native code and images 항목을 열어보면 코드들이 나올 것이다.
안드로이드 설정과 같은 방법으로 추가한다.
set -e
export NODE_BINARY=node
../node_modules/react-native/scripts/react-native-xcode.sh
+ ../node_modules/react-native-vector-image/strip_svgs.sh
+를 뺀
../node_modules/react-native-vector-image/strip_svgs.sh
이 부분만 추가한다.
SVG 파일 Import
import VectorImage from 'react-native-vector-image';
import { myAppIconImages } from '@src/iconImages';
const App = () => <VectorImage source={myAppIconImages.myAppLogo} />;
generate 명령어 실행
yarn react-native-vector-image generate
이렇게 진행을 하면 IOS와 Android 폴더에 파일들이 생성되는데
안드로이드 같은 경우
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M2.256,6.656C2.608,6.2933 3.0133,5.9947 3.472,5.76C3.9307,5.5147 4.4,5.3493 4.88,5.264C4.7627,5.7867 4.7253,6.3147 4.768,6.848C4.8213,7.3813 4.9547,7.8987 5.168,8.4L8,5.568C7.4987,5.3547 6.976,5.2213 6.432,5.168C5.92,5.1253 5.4027,5.1573 4.88,5.264C4.9867,4.7733 5.152,4.3093 5.376,3.872C5.6107,3.4133 5.904,3.008 6.256,2.656C6.9173,1.9947 7.7067,1.5413 8.624,1.296C9.52,1.072 10.4107,1.072 11.296,1.296C12.2133,1.5413 13.0027,1.9947 13.664,2.656C14.336,3.3173 14.7947,4.1067 15.04,5.024C15.264,5.92 15.264,6.8107 15.04,7.696C14.7947,8.6133 14.3413,9.4027 13.68,10.064C13.3067,10.4373 12.8907,10.7467 12.432,10.992C11.984,11.2267 11.5253,11.3813 11.056,11.456C11.1733,10.9333 11.2053,10.4053 11.152,9.872C11.1093,9.3387 10.9813,8.8213 10.768,8.32L8,11.232C9.12,11.6373 10.1653,11.7333 11.136,11.52C10.9013,12.5227 10.4373,13.3973 9.744,14.144C9.0827,14.8053 8.2933,15.2587 7.376,15.504C6.4907,15.728 5.6,15.728 4.704,15.504C3.7867,15.2587 2.9973,14.8053 2.336,14.144C1.6427,13.4507 1.1733,12.6453 0.928,11.728C0.6827,10.8427 0.672,9.952 0.896,9.056C1.1307,8.1493 1.584,7.3493 2.256,6.656ZM6.256,10.144C6.2987,10.176 6.3627,10.2293 6.448,10.304C6.704,10.5067 6.9067,10.6453 7.056,10.72L10.336,7.456C10.2613,7.3173 10.1227,7.1253 9.92,6.88L9.744,6.656L9.52,6.48C9.2747,6.2773 9.0827,6.1387 8.944,6.064L5.68,9.344C5.7547,9.4933 5.8933,9.696 6.096,9.952L6.256,10.144Z"
android:fillColor="#222222"/>
</vector>
각 SVG 이미지들 마다 이렇게 벡터 코드로 생성된 xml 파일이 생성된다.
IOS 같은 경우
각 SVG 이미지들 마다. imageset이라는 디렉터리가 생성되고 그 안에
{
"images": [
{
"filename": "light.pdf",
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
},
"properties": {
"preserves-vector-representation": true
}
}
이런 형태의 Contents.json과 이진 데이터로 생성된 light.pdf 파일 두 개가 생성된다.
yarn react-native run-ios
or
yarn react-native run-android
SVG 이미지를 통해 생성된 Android 및 iOS Native Vector 이미지가 잘 동작하며, recompile 과정에서도 문제가 없는 것을 확인할 수 있었다.
이 라이브러리를 사용하면 렌더링 속도를 향상하고 JS Bundle 크기를 줄일 수 있다고 하는데 실제로도 성능이 좋은 것 같았다. 앞으로 더 많이 사용해 보면서 혹은 react-native-svg 라이브러리를 사용해서 비교하면 이 라이브러리의 성능과 사용성을 더 잘 파악할 수 있을 것으로 생각된다.
이제 Dropbox에서 SVG 이미지들을 다운로드를 하고 그 이미지들로 Android와 IOS Native 벡터 이미지들까지 생성하고 사용하는 것을 완료했다. 이미지가 추가될 때마다 코드로 한 명령어씩 실행하면서 이 작업들을 매번 수행하기에는 비효율적이고 시간이 들기 때문에 이제 마지막으로 자동화 작업이 남았다.
우선 package.json 파일에 Dropbox에서 SVG 이미지들을 다운로드하는 JS 파일을 실행하는 명령어와 native assets 파일을 생성하는 명령어를 scripts 명령어로 만들어준다.
"fetch-dropbox": "node ./fetch-dropbox.js && yarn react-native-vector-image generate"
기존의 localazy의 번역 문구 동기화 & 자동화 작업과 같은 시간에 돌아가도록 스케줄 시간을 설정한다.
깃헙 코드를 내려받고 main 브런치로 체크아웃한다.
18 버전 노드를 셋업하고
dependencies를 설치한다
fetch-dropbox 명령어를 실행하고
변경된 사항들을 PR을 보낸다
이 작업들을 워크플로우 작업의 내용으로 명시해서 yml 파일을 생성하면 다음의 코드와 같다.
name: Dropbox Asset 업데이트
on:
schedule:
- cron: '0 23 * * 0-5'
- cron: '0 7 * * 0-5'
jobs:
fetch-dropbox:
name: Dropbox Asset 업데이트
runs-on: self-hosted-runner
steps:
- uses: actions/checkout@v3
with:
ref: main
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: yarn install
- name: Generate Asset
run: yarn fetch-dropbox
- uses: peter-evans/create-pull-request@v4
with:
token: ${{ token }}
commit-message: 'Dropbox Asset 업데이트'
branch: dropbox
title: 'Dropbox Asset 업데이트'
labels: labels
author: author
committer: committer
생각보다 간단하고 심플한데 이 코드가 지금까지 작업했던 일련의 긴 작업들을 자동으로 내가 원하는 시간에 실행되면서 자동화가 된다. 이렇게 되면 디자이너 분들이 SVG 이미지를 추가를 하더라도 나는 신경 쓸 필요가 없고 PR만 확인해서 Merge 후 추가된 이미지를 사용하면 된다!
Localazy 자동화 작업 이후, 오랜만에 자동화가 필요한 새로운 기능이 추가되어, 이전에는 유지보수 수준으로만 작업하던 것과는 다른 경험을 하게 되었다. 이번에는 Dropbox에서 시작하여 PR(풀 리퀘스트)을 자동화하는 모든 작업을 수행하게 되었는데, 이로써 나 자신뿐만 아니라 개발팀과 디자인팀 모두가 효율적으로 작업할 수 있게 되어 뿌듯했다. 아직 자동화해야 할 작업들이 거의 남아 있지 않는 것 같지만, 이러한 작업을 찾아내고 자동화해 나가는 것은 계속해서 진행해 보아야겠다.
참조:
https://help.dropbox.com/ko-kr/share/force-download
https://github.com/oblador/react-native-vector-image
https://yoon-talk.tistory.com/64