빵빵이의 일 두 배로 잘하는 법 ~!

신원규·2023년 12월 23일
11
post-thumbnail

취업난 기간에 열심히 준비해 간신히 프론트앤드 개발자로 합격한 빵빵이.
더는 길거리 시절로 돌아갈 수 없어 일을 열심히 쳐내가고 있던 와중..

어느날 갑자기 기획자 돈돈이가 화면을 보더니, 매인 메뉴를 표현하는 에셋 이미지가 보이지 않는다고 한다.
돈돈이: "빵빵아, 메뉴 화면이 안 보여.."
빵빵아 메뉴 화면이 안 보여

실시간으로 광고를 태워 들어오는 유저가 이탈을 해버리고 있는 절체절명의 상황!
빵빵이는 거지가 될 수 없기에 코드를 열심히 분석해본다.

누군가 프로젝트 안의 이미지파일 이름을 마구 바꿔버린 것을 발견하고,
빵빵이는 이쒸... 누구야... 라고 투덜거리며 깨진 이미지 경로를 수정한 뒤 배포를 하고 이슈 티켓을 닫으려는 순간

갑자기 미래의 로봇빵빵이가 나타나 빵빵이의 머리를 때려버린다.
미래의 로봇빵빵이

로봇빵빵이: "얀마 일 제대로 해!"

듣자하니 앞으로도 옥지가 마구 파일 경로를 바꿔버리며 유저는 자꾸 이탈하게 되고, 그 길로 다니는 스타트업은 폐업의 길로 접어들었다고 한다.
빵빵이는 그 뒤 거리를 전전하다가 납치되어 로봇빵빵이가 되어버렸다고 하는데..

빵빵이는 어이가 없었지만, 뭔 말을 하는지 일단 들어나 보기로 했다.

문제의 원인 분석하기

로봇빵빵이: "애초에 이미지 경로나 이름을 바꾼다고, 이미지가 안 보이는것 자체가 문제야.
이번 경로를 수정하는 건 언발에 오줌 누기 같은 임시방편밖에 안 되는 거지.
현상을 해결하려고 하지 말고, 문제의 원인을 파악해 다신 재현이 안 되도록 똑똑하게 일을 해봐."

빵빵이: "아니 그럼 어떻게 하란 말이에요? 프레임워크에서 파일 경로를 문자열로 받는 걸 제가 바꿀 순 없잖아요!"

로봇빵빵이: "파일 이름, 경로를 관리하는 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

profile
생존형 개발자. 어디에 던져져도 살아 남는것이 목표입니다.

6개의 댓글

comment-user-thumbnail
2023년 12월 24일

| "현상을 해결하려고 하지 말고, 문제의 원인을 파악해 다신 재현이 안 되도록 똑똑하게 일을 해봐."
이 말이 엄청 인상 깊네요ㅋㅋ 문제를 해결할 때, 현상 해결만 하고 다시 재발하는 현상들이 많은데 한번 더 깊게 고민할 수 있는 주제였어요!

1개의 답글
comment-user-thumbnail
2023년 12월 27일

플러터 주니어 개발자인데 되게 재미있게 봤어요! 잘보고있습니당!!

답글 달기
comment-user-thumbnail
2024년 6월 4일

로봇빵빵이의 조언에 따라 스크립트를 작성하는 것은 정말 현명한 선택이었습니다. 이를 통해 이미지 및 폰트 파일과 같은 에셋을 효율적으로 관리하고, 잠재적인 오류를 방지할 수 있게 되었습니다.
mapquest

답글 달기
comment-user-thumbnail
2024년 8월 9일

행사가 재미있을 것 같아서 갈 계획을 세울게요 slope

답글 달기
comment-user-thumbnail
2024년 10월 23일

Losing users due to code problems is serious, and I hope 빵빵이 manages to resolve temple run it.

답글 달기