취업난 기간에 열심히 준비해 간신히 프론트앤드 개발자로 합격한 빵빵이.
더는 길거리 시절로 돌아갈 수 없어 일을 열심히 쳐내가고 있던 와중..
어느날 갑자기 기획자 돈돈이가 화면을 보더니, 매인 메뉴를 표현하는 에셋 이미지가 보이지 않는다고 한다.
돈돈이: "빵빵아, 메뉴 화면이 안 보여.."
실시간으로 광고를 태워 들어오는 유저가 이탈을 해버리고 있는 절체절명의 상황!
빵빵이는 거지가 될 수 없기에 코드를 열심히 분석해본다.
누군가 프로젝트 안의 이미지파일 이름을 마구 바꿔버린 것을 발견하고,
빵빵이는 이쒸... 누구야... 라고 투덜거리며 깨진 이미지 경로를 수정한 뒤 배포를 하고 이슈 티켓을 닫으려는 순간
갑자기 미래의 로봇빵빵이가 나타나 빵빵이의 머리를 때려버린다.
로봇빵빵이: "얀마 일 제대로 해!"
듣자하니 앞으로도 옥지가 마구 파일 경로를 바꿔버리며 유저는 자꾸 이탈하게 되고, 그 길로 다니는 스타트업은 폐업의 길로 접어들었다고 한다.
빵빵이는 그 뒤 거리를 전전하다가 납치되어 로봇빵빵이가 되어버렸다고 하는데..
빵빵이는 어이가 없었지만, 뭔 말을 하는지 일단 들어나 보기로 했다.
로봇빵빵이: "애초에 이미지 경로나 이름을 바꾼다고, 이미지가 안 보이는것 자체가 문제야.
이번 경로를 수정하는 건 언발에 오줌 누기 같은 임시방편밖에 안 되는 거지.
현상을 해결하려고 하지 말고, 문제의 원인을 파악해 다신 재현이 안 되도록 똑똑하게 일을 해봐."
빵빵이: "아니 그럼 어떻게 하란 말이에요? 프레임워크에서 파일 경로를 문자열로 받는 걸 제가 바꿀 순 없잖아요!"
로봇빵빵이: "파일 이름, 경로를 관리하는 enum을 생성해서 경로를 바로 입력하는 게 아니라 이넘의 값을 넘겨주는 식으로 바꾸면 되잖아."
/// 예제의 코드는 Dart, Flutter 이지만,
/// 아이디어 자체는 모든 언어, 프레임워크에서 사용할 수 있습니다!
enum AssetFiles {
TEXT_LOGO('somePath/text_logo.svg')
...
SPlASH_IMG('somePath/text_logo.png');
final String path;
const AssetFiles(this.path);
}
빵빵이는 억울하다.
빵빵이: "이씌... 그래도, 이넘안의 경로가 오타가 나거나, 위치를 바꿔버리고 반영하지 않는다면 오류가 나는 건 똑같잖아요!"
로봇 빵빵이: "그렇다면, 에셋 파일 이넘을 관리하는 코드를 자동으로 생성하면 되는 거잖아."
빵빵이: "그걸... 어떻게 해요?"
로봇빵빵이: "이미지, 폰트파일등은 하나의 "assets" 라는 디렉토리 안에서 관리하지.
몇개 의 파일은 2~3단계의 디렉토리로 감싸져 있지만, 이거 우리 알고리즘 공부할 때 봤었던 트리구조하고 같잖아.
그렇다면, assets 디렉토리 밑의 모든 파일을 가져오는 문제는, 트리를 순회하며 모든 leaf node 리스트를 가져오는 문제로 볼 수 있지 않겠니."
빵빵이: "아, 그러면 스크립트 안에서 제가 처음에 asset 디렉토리를 찾아올 수 있도록 설정한 이후, 그걸 기반으로 DFS 서치를 하면 되는거 군요! "
void main() {
try{
final Directroy assetRootDir = Directroy("rootpath");
final Directroy genDir = Directroy("genDirPath");
final File outputFile = File("genDirPath/asset_paths.dart");
final List<File> files = [];
collectFilesDFS(assetRootDir, files);
}
}
void collectFilesDFS(final Directory dir, final List<File> fileList) {
final Lst<FileSystemEntity> entities = dir.listSync();
// 자식 노드를 순회하며
for (final FileSystemEntity entitiy in entities) {
if (entity is Directory) {
// 자식 노드가 디렉토리면 재귀호출을,
collectFilesDFS(entity, fileList);
continue;
}
if (entity is File) {
// 자식 노드가 파일이면 리스트에 추가한다.
fileList.add(entity);
}
}
}
빵빵이: "가져오는 건 알겠는데... 이걸 어떻게 enum 으로 만들죠?"
로봇빵빵이: "문자열의 형태로 가공한 뒤, outputFile 에 쓰면 되는 거지."
void main () {
...
final File outputFile = File("genDirPath/asset_paths.dart");
final List<File> files = [];
collectFilesDFS(assetRootDir, files);
// 가져온 file list 를 다음의 형태로 가공하는 코드
// enumEntties = " FILE01_SVG(filePath),
// ...,
// FILE_PNG(filePath),";
final String enumEntries = files.map((final File file) {
final String? path = regex.fistMatch(file.path)?.group(0);
if (path == null) return '';
return " ${path.toUpperCase().replaceAll("/", '_').replaceAll('.', '_')} (\'assets/$path\')".join(',\n');
});
// outputFile 의 경로에 가공한 코드를 저장한다.
outputFile.wirteAsStringSync("""enum AssetPath {
${'assets';}
const AssetPaths(this.path);
final String path;
}""");
}
빵빵이: "좋아요. 이제 스크립트는 작성했는데, 이걸 어떻게 자동으로 실행시키게 하죠?"
로봇빵빵이: "package.json 이나, Flutter 의 경우에는 melos.yaml 파일의 스크립트를 바꾸면 되는 거야."
// json 의 경우
"scripts": {
"start": "node asset_codegen.js && react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
// yaml 의 경우
scripts:
run: dart run asset_codegen.dart && flutter run
빵빵이는 문제를 해결해서 기쁜 마음으로 PR 을 작성해 병합한 이후, 소주나 뿌시러 퇴근하려고 했다.
그때, 로봇빵빵이가 빵빵이의 머리를 한번더 때려버리는데..
빵빵이: "아씨 왜 때려요!"
로봇빵빵이: "옥지를 봐봐."
옥지는 PR이 병합되어서 도구가 제공되었지만, 그런 건 신경 안 쓰고 이전 방식 그대로 코드를 적고 있는 것이다.
그렇다. 빵빵이는 아무리 도구를 제공한다고 하더라도 동료가 쓰지 않으면 운영상 리스크는 해결되지 못하는 것을 알아버렸다.
여전히 로봇빵빵이가 되는 미래는 바뀌지 않은 것이다!
빵빵이: "옥지얌... 내가 올린 PR 봤어..?"
옥지: "뭐"
빵빵이: "아...아니얌..."
빵빵이는 옥지의 주먹이 무서워 차마 말하지는 못하고, 투덜거리며 자리에 돌아온 뒤
옥지가 AssetPath enum 을 사용할 수 밖에 없도록 만들어, 미래를 바꾸겠다고 다짐한다.
빵빵이: "로봇빵빵이님.. 어떡하면 옥지를 제가 만든 이넘을 쓰도록 만들 수 있을까요?"
로봇빵빵이: "커스텀 린트를 만들어서, AssetPath 를 쓰지 않으면 CI를 통과하지 못하도록 만들어."
빵빵이: "그거.. 어떻게 하는 거죠?"
로봇빵빵이: "모든 프레임워크는 자신만의 analyze rule 을 설정하는 방법을 제공해.
Dart 의 경우는 analyze Plugin 의 형태로 제공하는데, 이걸 직접 활용하는것 보다는 좀더 사용하기 좋은 도구를 제공해주는 custom_lint 를 사용할 수 있지."
로봇 빵빵이는 커스텀 린트 패키지를 새로 만든 뒤, 커스텀 린트를 작성하는 법을 알려줬다.
import 'package:custom_lint_builder/custom_lint_builder.dart';
PluginBase createPlugin() => _빵빵이플러그인();
final class _빵빵이플러그인 extends Pluginbase {
List<LintRule> getLintRules (final CustomListConifgs configs) => [
옥지얌_코드_그렇게짜는거_아니야(),
];
}
final class 옥지얌_코드_그렇게짜는거_아니야 extends DartLintRule {
옥지얌_코드_그렇게짜는거_아니야() : super(code:_code);
static const LintCode _code = LintCode(
name: "옥지얌... 코드 그렇게 짜는거 아니야...",
problemMessage: "내가 만든 AssetPath enum 사용해줘...",
);
void run(
final CustomLintResoler resolver,
final ErrorReporter reporter,
final CustomLintContext context,
) {
context.registry
.addInstanceCreationExpression((final InstanceCreationExpression node) {
// SvgPicture 인지 확인하는 방법.
final bool isSvgPicture = node.staticType?.getDisplayString(withNullablity: false) == 'SvgPicture';
// Image 인지 확인하는 방법.
final bool isImage = node.staticType?.getDisplayString(withNullability: false) == 'Image';
// asset 이미지를 불러오고있는지 확인하는 방법.
final bool isAsset = node.constructorName.name?.name.contiains('asset') ?? false;
// svg 나 png 파일을 불러오면
if ((isSvgPicture || isImage) && isAsset) {
// 생산자 함수에 제공되는 인자중에 "AssetPaths" 인 인자가 있는지 검사 한 후
final bool isAssetPathContains = node.argementList.arguments
.map((final Expression e) => e.begin.lexeme).contains("AssetPaths");
// 포함되어있지 않다면 린트에 오류를 보고한다.
if (!isAssetPathContains) {
reporter.reportErrorForNode(_code, node);
}
}
}
}
}
작성한 린트를 프로젝트에 적용하니, 옥지는 더 이상 AssetPath enum 을 사용하지 않고는 배길 수 없어졌다!
빵빵이는 로봇빵빵이의 도움으로 정말 문제를 해결하고 재발을 방지하기 위해서는 현상을 보는 것이 아닌, 원인을 분석하고 이를 기술적으로 재현 방지를 할 수 있도록 팀에게 기여해야 한다는 교훈을 얻게 되었다.
그리하여 어렵게 취업한 스타트업이 파산하지 않아, 로봇빵빵이로 변하는 불행한 미래는 사라지고 빵빵이는 옥지랑 행복하게 살았다고 한다~
2023.12.24 추가)
이 주제와 관련해 제가 실제로 사이드 프로젝트에서 작성한 코드가 궁금하신분들은
아래의 PR 을 참고해보셔도 좋을것 같습니다!
https://github.com/L1A-crew/zzekak_client/pull/6
로봇빵빵이의 조언에 따라 스크립트를 작성하는 것은 정말 현명한 선택이었습니다. 이를 통해 이미지 및 폰트 파일과 같은 에셋을 효율적으로 관리하고, 잠재적인 오류를 방지할 수 있게 되었습니다.
mapquest
Losing users due to code problems is serious, and I hope 빵빵이 manages to resolve temple run it.
| "현상을 해결하려고 하지 말고, 문제의 원인을 파악해 다신 재현이 안 되도록 똑똑하게 일을 해봐."
이 말이 엄청 인상 깊네요ㅋㅋ 문제를 해결할 때, 현상 해결만 하고 다시 재발하는 현상들이 많은데 한번 더 깊게 고민할 수 있는 주제였어요!