개발자에서 엔지니어로 거듭나기 1탄

나는 그동안 소프트웨어 개발자와 소프트웨어 엔지니어를 구분해서 다른 역할이라고 생각하지는 않았다. 왜냐면 현실에서 내가 개발자로 불리든 엔지니어로 불리든, 하는 일은 늘 똑같았기 때문이다. 아니 똑같다고 생각하고 살아왔다. 하지만 “구글 엔지니어는 이렇게 일한다” 라는 책을 읽으면서 조금 다르게 인식하는 개기가 되었다.

팀에서 이 책을 함께 읽기 시작한 건 2022년 6월 말이었다. 입사하고 얼마 되지 않아 한창 개발 프로세스를 개선하고 있던 때 였는데, 책을 읽으면서 그동안 해왔던 일들이 헛짓꺼리가 아니었음을 위로받고, 공감하면서 책을 봤던것 같다. 이때부터 퇴직하는 순간까지 팀으로 많은 일을 해왔다. 이 글을 쓰고 있는 이유는 단순히 그때 했던 일을 기록하기 위함이 아니다. 그때 나눴던 이야기! 그때 그 결정에 기반이 됐던 팀의 멘탈리티는 지금껏 경험하지 못한 것들이었고 앞으로도 지속하고 싶은 것들이라 잊지 않기 위해 그리고 그 손에 잡히지 않는 그 무엇을 소개하고 싶었다.

지속 가능한 엔지니어링

아무튼 나는 경험으로 배우는 성향탓인지 그동안 자연스럽게 해온 일들이 누군가에 의해서 이론으로 정립되면 아하! 하고 무릎을 탁치게 되는데, 이 책을 읽는 내내 그랬다. 특히 프로그래밍과 소프트웨어 엔지니어링의 차이를 명확하게 정의하고 써내려간 부분에서 확 끌려버렸다. 왜냐면 그동안 팀에서 해왔던 의사결정들이 모두 장기적인 관점에서 지속가능여부를 늘 함께 고민하며 결정했기 때문이다.

우리가 개발 환경 개선에 목메는 이유

우리가 제품을 지속적으로 개발하기 위해 제일 먼저 한 일은 개발환경개선이었다. 이에 대한 자세한 얘기는 다른 글에서 소개했으니 여기선 왜 이 개선이 중요한지 얘기해보고자한다. 당신이 개발자라면 굳이 아래 설명을 안 읽어봐도 알겠지만 비 개발자라면 개발자들이 왜 개발 환경(개발장비포함) 개선에 미쳐있는지 꼼꼼히 그래프를 한번 보시라!

개발자가 만들어낸 버그의 85%는 코딩하는 과정에서 발생한다.

그리고 생산된 결함이 발견될 확률은 제품의 릴리즈 시기에 다가올수록 높아진다. 하지만 한번 제품이 릴리즈되면 유저로 부터 버그 리포팅이 쉽게 오지 않기 때문에 결함을 발견할 확률은 급격히 떨어진다. (주황색선)

반면에 결함 수정 비용은 제품 사이클 뒤로 갈수록 기아급수적으로 증가한다.(빨간색선) 왜냐하면 QA 담당자가 개발자를 위해 테스트를 해주고 재현 스탭을 꼼꼼히 적어주고, 이슈를 생성해서 전달하면 개발자는 수정하고 다시 테스트를 반복하기 때문에 이 모든 과정이 비용으로 잡힌다.

따라서 결함을 찾는 노력을 앞으로 땡길수록 수정 비용도 줄고 결과적으로 결함도 더 적게 발생한다. (주황색선)

그림의 구체적인 수치를 몰라도 개발자라면 경험적으로 익히 알고 있는 내용이다. 프로젝트를 시작할때 개발 환경을 제일 먼저 설정하는 이유도 위와 같은 맥락에서 결함을 좀 더 빨리 찾기 위함이다. 하지만 이미 운영중인 프로젝트에 참여하게 되면 어떤 개선이든 쉽게 되는건 하나도 없다.

특히나 배경에 대해 물어볼 사람 하나 없는 환경에 처하면 더욱 난처해지는데 우리에겐 되려 약이 되었다. 한번은 연말 회고 중에 팀원들과 이런 이야기를 나눈적이 있다. 만약에 기존 팀 멤버가 한명이라도 존재했다면 우리가 이렇게 과감하게 개선을 할수있었을까? 아마도 더 어려웠을꺼라는 의견이 지배적이었다. 이유는 질의를 통해 파악된 과거 맥락에 시각이 좁아지는 것도 문제지만 그것보다 사람 일이라는게 다 그렇다. 감정을 건드리면 쉽게 될일도 어렵게 된다.

과거의 내 경험도 그러했다. 보통 새로운 사람이 합류하면 변화가 시작되는데, 아이러니하게 개선이라는 작업이 때론 누구가의 업적을 지우는 행위가 되기 때문에 팀의 유대나 신뢰가 충분하지 않으면 개선은 곧 개인에 대한 공격으로 인식되기도한다는 사실을 그땐 몰랐다. 그래서 사람이 하는 일 이라는게 참으로 어렵지만 그래서 또 더 나은 사람이 되기도한다

여튼 물어볼사람이 없고, 무식하면 용감하다고 일단 해보게 되는데 그렇게 접근했다 몇번 롤백을 하다보니 이제는 어떤 개선이든 아래와 같은 질문으로 시작한다.

  1. 외부 의존성 없이 팀 안에서 해결할 수 있는가? 의존성이 있다면 의존성을 먼저 제거한다.
  2. 예정된 릴리즈 안에 해결할 수 있는 문제인가? 큰 사이즈면 계획을 세워 쪼갠다.
  3. 내가(우리가) 욕심을 부리고 있는 것은 아닌가? 욕심이라 판단되면 하지 않는다!
  4. 내가 욕심을 내야 할 만큼 장기적인 큰 이득이 있는가? 큰 이득이 확실하면 도전한다!

전략적으로 ESLint 적용하기

대표적으로 쉽게 봤다 피 본 사례가 ESLint 적용이었다. 모던 개발환경에선 상상도 할 수 없는 일이겠지만 이런게 현실이다. 특히 개발 환경은 한번 설정하면 잘 건드리지 않기 때문에 지속 관리 대상에서 잊혀지기도 쉽다. 프로젝트 초기에 설정한 가장 모던(Morden)했던 개발 환경이 5년지나고 10년 지난 뒤에도 모던 할 리가 없다.

기술의 발전은 늘 과거의 문제를 해결하기 때문에 새로운 도구가 나오면 관심을 가져야한다. TSLint 개발팀도 여러가지 이유로 TSLint를 2019년에 ESLint 에 통합했다. React 공식 문서에 더이상 create-react-app 을 추천하지 않는 이유도 다 기술 발전의 산물이다. 이런 의미에서 타입스크립트 버전도 올리고 ESLint 도 적용하고 싶었다. 그런데 타입스크립트 문법이 바뀌는 복병을 만났다.

try {
}catch(ex) {
   여기서 ex 는 any 타입에서 unknown 타입으로 변경됨. 
   즉, 개발자가 직접 ex 의 값을 캐스팅해야함. 
   => 한땀한땀 수정하는데만 하루 꼬박 걸림.
}

타입스크립트 4.7버전부터 Exception 에러를 더 이상 any 로 캐스팅 할 수 없게 됐다. 이게 은근 스트레스인게 코드를 한땀 한땀 바꾸기도 해야하지만 에러 처리 로직이 변경될 확률도 높아진다. 처음엔 단순히 개발 환경을 개선하려고 한 것 뿐인데 어느 순간 로직이 변경되면서 갈등의 순간이 찾아온다. 결론은 롤백!

그렇게 6시간을 날렸다. 한번 실패하면 전략을 바꾸게된다. 일단 타입스크립트 버전업을 포기하고 ESLint 부터 적용한다. 적용하고나면 역시나 줄줄이 빨간팬 선생님의 첨삭 지도를 받게 되는데 이게 또 아무 생각 없이 고치다보면 필연적으로 로직을 변경하게 된다. 그러면 또 이걸 PR 해야하나 롤백 해야하나 갈림길에 놓인다. 아무런 기능 변경이 없는데 로직 변경으로 QA 팀에게 테스트를 요청할 순 없다. 이때는 과감히 빨간팬 구독을 취소한다. 미안한데요. 저는 당분간 그냥 이렇게 살께요.

"rules": {
    // 모듈 권고사항
    "import/no-unresolved": 0,
    "import/namespace": 0,
    "import/default": 0,
    "import/export": 0,

    // 모듈 권장사항
    "import/no-duplicates": 0,
    "import/no-named-as-default": 0,
    "import/no-named-as-default-member": 0,

    // JS 권고사항
    "for-direction": 0,
    "prefer-const": 0,
    "prefer-spread": 0,
    "prefer-rest-params": 0,
    "no-empty": 0,
    "no-var": 0,
    "no-empty-pattern": 0,
    "no-self-assign": 0,
    "no-useless-catch": 0,
    "no-prototype-builtins": 0,
    "no-inner-declarations": 0,
    "no-fallthrough": 0,
    "no-extra-boolean-cast": 0,
    "no-useless-escape": 0,
    "no-case-declarations": 0,
    "no-constant-condition": 0,
    "no-unsafe-optional-chaining": 0,
    "no-unsafe-finally": 0,
    "no-async-promise-executor": 0,

    // TS 권고사항
    "@typescript-eslint/ban-types": 0,
    "@typescript-eslint/no-empty-function": 0,
    "@typescript-eslint/no-inferrable-types": 0,
    "@typescript-eslint/no-this-alias": 0,
    "@typescript-eslint/no-var-requires": 0,
    "@typescript-eslint/no-namespace": 0,
    "@typescript-eslint/no-unsafe-finally": 0,
    "@typescript-eslint/no-non-null-asserted-optional-chain": 0,
    "@typescript-eslint/no-empty-interface": 0,
    "@typescript-eslint/triple-slash-reference": 0,
    "@typescript-eslint/adjacent-overload-signatures": 0,
    "@typescript-eslint/ban-ts-comment": 0,

    // TS 권장사항
    "@typescript-eslint/no-explicit-any": 0,
    "@typescript-eslint/no-unused-vars": 0,
    "@typescript-eslint/no-non-null-assertion": 0,

    // 리액트 권고사항
    "react/display-name": 0,
    "react/prop-types": 0,

    // 웹접근성 권고사항
    "jsx-a11y/click-events-have-key-events": 0,
    "jsx-a11y/alt-text": 0,
    "jsx-a11y/anchor-is-valid": 0,
    "jsx-a11y/no-static-element-interactions": 0,
    "jsx-a11y/no-noninteractive-element-interactions": 0,

    // 그밖에 권고사항
    "etc/no-implicit-any-catch": 0,
    "etc/no-assign-mutated-array": 0,

    // 그밖에 권장사항
    "etc/no-deprecated": 0
  },

예외 조항이 한 바닥이지만 ESLint 가 일단 적용된 상태기 때문에 여유가 생길때마다 예외사항을 하나씩 제거하는 전략을 취할 수 있게 됐다.

Sonar Cloud 로 코드 리뷰 받기

최근엔 Sonar Cloud를 깃헙에 적용했는데 이것도 신세계다. 2010년도 쯤만해도 코드 퀄러티를 위해 소나큐브를 적용하겠다고 하면 개발자들의 불만이 쏟아졌다. 당시에 소나가 주는 코드 점수가 평가의 잣대로 쓰일것만 같은 분위기가 실제로 있었다. 고백하건데 나도 사실 부정적이긴했다. 그런데 10년만에 나도 변했고 세상도 변했다.

코파일럿과 더불어 소나 정도는 충분히 투자할만한 가치가 있다. 소나 클라우드를 적용하면 아래와 같이 PR 할때마다 정적분석해서 문제점을 리포팅해준다. PR 단계에서 변경된 코드만 정적분석을 하기 때문에 마치 코드 리뷰를 받는 느낌도 든다.

타입 추론을 기반으로 기존 구현을 검증하기

타입스크립트의 타입 추론 능력이 점점 개선되어 이제는 타입을 굳이 지정하지 않아도 될 만큼 추론 능력이 뛰어나다. 타입 추론 능력에 대한 상세한 내용은 GTP 에게 각자 물어보자.

  // 이제 return 타입을 안 써도 된다.
  function add(a: number, b: number): string {
    return a + b;
  }

자바스크립트 프로젝트를 타입스크립트로 전환한 프로젝트라면 타입 검사를 철저하게 지정해서 쓰기가 쉽지가 않다. 예를 들면 any 사용을 막는다던지 strictNullChecks 를 강제한다든지 이런 컴파일 옵션을 켜는 순간 수많은 에러를 만나기 때문에 프로젝트 진행 자체가 안되는 경우도 많다. 이런 경우 위에서 살펴본 ESLint 적용과 마찬가지로 기술부채를 안고 현재 프로젝트에 여러가지 예외 사항을 지정해서 점진적 개선 계획을 세우게 되는데, 점진적 개선 계획이 틀어지는 순간(팀원 이탈 혹은 팀의 해체) 코드는 지뢰밭이 되고 만다.

예를 들면 함수에 지정한 반환 타입이 TS가 추론한 반환 타입과 다른 경우가 여러가지 이유로 존재할수있다. 예를 들면 any 를 강제하지 않고, any를 사용했다가 다른 타입으로 캐스팅을 했다면 추론 내용이 달라지기도 한다. 협업자들과 인터페이스를 맞추고 구현을 미뤄두는 경우에도 비슷한 양상으로 흐르게 되는데, 예를 들면 인터페이스를 맞추기 위해 any로 강제 캐스팅 한다거나 any를 다시 다른 타입으로 캐스팅하게 되는 순간 지뢰를 하나 심는 꼴이 된다.

레거시 코드에서 이런 지뢰를 발견할 때마다 지정된 반환 타입을 의심하게 된다. 이 함수가 정말로 문자열(string)을 반환하는가? 아니면 프로미스(Promise<string>)를 반환하는가? 구현 내용을 읽어봐도 확신이 서지 않으면, 반환 타입을 지우고 추론된 타입과 같은지 교차검증을 한다. 단순히 타입이 제대로 지정되어 있는지 확인하려던 일이 어느새 원인을 알 수 없었던 오래된 문제의 핵심에 다가가고 있었다.

이와 관련된 대표적인 사례가 불완전한 파이 저장 문제였다. 당시 “원인을 알 수 없는 오염된 파이”라는 제목으로 오랜동안 해결하지 못한 고객 문의가 있었는데… 이 문제는 파이를 저장 할때 데이터와 리소스를 zip 파일로 묶는 로직에서 발생했다. 리소스 파일중에는 현재 프리뷰 화면을 캡쳐한 파일이 있는데, 캡처 로직은 별도의 랜더러에서 동작하기 때문에 명확하게 비동기로 동작한다. 하지만 비동기함수 임에도 반환 타입이 프로미스로 지정되어 있지 않았기 때문에 동기식으로 동작하는 zip 파일 엔트리 과정에서 타입 오류를 발생시키지 못했다. 이로 인해 저장 로직은 화면 캡쳐가 얼마나 지연 되는지에 따라 성공 여부가 갈리는 치명적인 결함을 갖게된다.

기존 개발팀은 잘못 쓰인 반환 타입이 원인이란 사실을 쉽게 찾지 못했던것 같다. 이런 현상은 내가 지난 2년 내내 팀에서 겪었던 어려움이기도 했다. 보통 개발자는 자신이 구현한 코드에 갇히기 마련이라 좀처럼 새로운 시간으로 문제를 접근하지 못한다. 그래서 우리팀은 너나 할것 없이 일정시간 투자해서 문제를 해결하지 못하면 동료들을 소환했다.

반복되는 개발 과정(Coding)을 더 빠르게 개선하기

앞에서 우리가 발견하게되는 결함의 85%는 코드를 작성하는 과정에서 만들어진다고 했다. 한마디로 코드를 잘못 짜서 버그가 만들어진다는 얘기다. 그렇기 때문에 일단 문제가 생기면 모두 다 내탓이요! 라고 생각하면 대부분 맞다. 그렇다면 애초에 결함없는 코드를 작성해야하는데, 그 방법은 역설적으로 그 결함을 내가 더 빨리 찾으면 된다.

결함을 빠르게 확인하는 가장 원시적인 방법은 코드를 작성하고 런타임 에러를 눈으로 확인하는 방법이다. 이 방법의 종착지는 결국 코드 변경시 눈으로 바로 확인할수있는 모던한 개발 환경을 갖추는 일이된다.

테스트 코드를 통해서도 결함을 쉽게 찾을 수 있는데, TS로 코드를 작성하다보면 테스트 코드도 TS로 작성하고 싶은 욕구가 생긴다. 하지만 테스트 코드를 TS로 작성하게 되면 매번 테스트할때마다 컴파일을 먼저 해야하기 때문에 상당히 느리다. 우리팀은 이 문제 해결을 위해 SWC를 도입하게 됐다. 멀티 코어를 쓸수있는 SWC 러스트 컴파일러는 확실한 성능을 보장한다. 실제 우리 테스트 코드도 내 로컬 환경(맥스튜디오 M2 Max)에서 전체 풀 테스트가 6분 이상 걸렸는데, SWC를 적용하고 1분 내외로 6배 빨라졌다.

다만 SWC 컴파일러가 속도에 중점을 둔 만큼 예민하다. 예를 들면 순환 참조가 있으면 컴파일을 제대로 하지 못하는데, SWC를 적용하면서 자연스럽게 우리 코드의 순환 참조 문제가 해결되었다.

불필요한 논쟁피하기

개발하다보면 사실 별것 아닌것 가지고 논쟁하는 경우가 많은데, 그 중에 하나가 바로 브랜치 전략이다. 아마도 혼자 개발해본 사람이라면 대부분 공감하는 이야기라 생각하는데 브랜치 따고 머지하는 일이 다 비용이기 때문이다. 혼자 개발할 땐 역시 main 브랜치 하나에 커밋을 직접 쌓는게 가장 효율적인데 놀랍게도 여럿이 개발해도 대부분 이 전략이 유효하다는 사실이다.

경험적으로 main 브랜치 하나를 유지하고, 기능 브랜치(PR 브랜치라고도 부른다)를 main 에서 따서 PR 후에 다시 main 으로 스쿼시 머지하면 main 브랜치의 모든 커밋이 PR 단위로 보기좋게 쌓이게 된다. 이때 릴리즈가 필요하면 main 브랜치에서 배포하고 추가로 버전을 태깅한다.

이것을 기본 골자로 여러 상황에 응용할수있는데 모든 의사결정은 비용을 최소화하는 방향으로 결정한다. 예를 들어, 어떤 기능 A를 개발하기 시작했고, 작은 이슈 3개로 나눠서 2개의 PR을 이미 main 에 반영했다. 그런데 이번 릴리즈에서 기능 A의 마지막 PR을 마무리 짓지 못해 배포에서 제외하기로 했다. 이런 경우 취할수있는 방법을 생각해보자.

  1. main 브랜치에 반영된 커밋 2개를 먼저 리버트한 후, 새로운 기능 브랜치에 체리픽해서 남은 기능을 개발한다.
  2. 마무리짓지 못한 마지막 PR에 기능 플래그를 달아서 main 병합하고, 남은 기능을 새로 브랜칭해서 개발한다.

1번은 기능 A를 릴리즈에서 제외하는 아주 깔끔한 방법이지만 기능 A 개발이 미뤄지면서 다른 기능과 충돌할 가능성이 더 높아진다. 반면 2번은 미완성 코드가 함께 배포되는 문제가 있지만 플래그를 달아두기 때문에 A 기능이 빠지는 효과는 동일하고 충돌가능성은 사라진다. 비용 측면에서 보면 플래그를 추가하는 일이 나중에 충돌났을때 해결하는 비용보다 보통 더 작기 때문에 우리는 거의 모든 상황에서 2번을 택했다.

또 다른 상황을 가정해보자. 한참 개발중에 이전에 배포한 버전에서 치명적인 문제가 발생해 핫픽스를 해야하는 상황이 생겼다. 이런 경우에도 최소 비용을 따져본다. 이전에 배포한 해쉬에 태깅을 해뒀으므로 이전 태그 버전에서 release/v7.x 형태로 브랜칭하고 문제를 해결한다. 해결된 후에는 바로 릴리즈 브랜치에서 핫픽스 배포를 진행한다. 이렇게 하면 일단 치명적인 문제는 해결된 상황이다.

이후 작업은 다시 비용을 따져본다. 왜냐면 핫픽스 배포는 늘 시간에 쫒겨서 진행되기 때문에 또다른 핫픽스를 만들어내는 경우도 있다. 혹은 이전 릴리즈 버전에만 해당 문제가 발생하고 이미 main 브랜치에 문제가 해결된 경우도 있기 때문에 핫픽스 변경 내용을 다시 main 에 체리픽 하지 않아도 되는 상황이 있을수도 있다. 때로는 릴리즈 브랜치에서 핫픽스 한 후에 릴리즈 브랜치를 지우는것 자체가 비용으로 느껴지기로 했다. 그래서 우리는 릴리즈 브랜치를 다음 릴리즈 브랜치가 생기기 전까지 남겨두기로 결정했다.

이렇듯 우리의 브랜치 전략은 어떤 원칙을 세우고 무조건 그 흐름을 지킨다기 보다는 단일 브랜치 유지라는 대전제 하나만 두고 나머지는 최소비용으로만 관리했다. 물론 이렇게 결정한 이유에는 팀의 두 시니어가 브랜치도 비용이라는 생각이 같았기 때문에 비교적 논쟁없이 합의할 수 있었다. 그리고 놀랍게도 이런 브랜치 전략을 Trunk-Based Development 라고 부른다는 사실을 나중에서야 알았다. ㅎㅎㅎ

나머지 이야기는 2탄에서… To be continue…

다시 개발자로 돌아가기

팀을 이끌어야하는 역할을 명시적으로 부여 받았을때 실무의 비중을 얼마나 두어야할까? 과연 실무를 내려놓고 개발자가 매니징만 할 수 있을까? 한동안 답을 찾지 못했다. 선배들에게 조언을 구하면 늘 그 자리는 코드의 욕심을 내려놓아야한다는 이야기를 주로 들었다. 하지만 난 그때 실무를 내려놓지 못했다. 누군가를 믿지 못해서가 아니다. 실무는 내게 수많은 맥락을 제공한다. 그리고 그 맥락을 놓치면 나는 그 어떤 의사결정도 제대로 못할 것만 같은 두려움이 있었다.

그래서 실무를 모두 내려놓은 매니저의 삶도 살아봐야겠다는 생각도 했다. 물론 1년을 채 버티지 못하고 스스로 물러났지만 짧은 시간동안 큰 깨달음 얻고 내려왔으니 운이 좋았다. 이 과정에서 내가 배운건 개발자나 매니저나 본질은 똑같다는 거다. 결국 다 회사가 성공하기 위해 필요한 역할에 지나지 않는다. 그동안 조직장을 마치 상위 포지션이라고 생각했던게 나의 큰 착각이었다. 조직 구조도 결국 회사의 성공 확률을 높이기 위해 펼치는 여러 전략중에 하나일 뿐이다. 그러니 회사가 성장할때마다 조직개편을 하는거 아니겠는가?

아무튼 다시 개발자로 돌아왔다. 매니저에서 다시 개발자로 돌아갈땐 아무런 직책도 맡고 싶지 않았다. 그저 방망이 깎는 노인이고 싶었다. 하지만 현실은 나를 그렇게 쉽게 내버려두지 않는다. 이미 내 커리어에 조직장 경력이 있으니 회사는 나에게 리더의 역할도 은근히 기대한다. 나 또한 눈치가 있으니 그걸 모르진 않는다. 하지만 나도 다 계획이 있다.

팀빌딩의 시작

이번 스테이지는 순수하게 개발자로 다시 평가받고 싶었다. 그리고 조직장과 같은 직책 없이 열정 많은 한 개발자가 다른 팀원들에게 어떤 영향을 주는지 테스트해보고 싶었다. 그래서 오로지 개발만 할꺼라고 선을 긋고 입사했다. 어마어마한 코드베이스가 입사 첫날부터 나를 억누른다. 오래된 코드베이스는 전혀 트랜디하지 않았다. 코드를 수정하면 결과를 확인하는데까지 대략 30초. UI 개발에 코드 한줄 고치고 맞는지 확인하는데 30초는 어마어마한 시간이다. 결국 머리로 컴파일 하거나 개발자 도구를 열어서 직접 스타일을 바꾸고 확인한 후에 코드를 수정하고 다시 30초. 몇번을 반복하면 수많은 생각이 떠오른다. 아.. 이래서 다 그만 둔건가? 왜 이걸 아직까지 안고치고 있었던거지? 하아… 한숨이 절로 새어나오고 지금 생각해도 가슴이 턱턱 막힌다. 개발 환경을 트랜디하게 바꾸지 못하면 결국 아무것도 하지 못한다는 각오을 몇번이나 되새기며 시도하고 실패하고 롤백을 두번이나 했다. 그렇게 두번 연습하니 세번짼 성공 하더라. 진짜 혼자였으면 못했다.

한 PR에 추가된 코드가 만줄 삭제된 코드가 오천줄…..

개발 환경이 좀 개선됐다고 갑자기 개발속도가 빨라지진 않는다. 거대한 코드베이스 그 자체가 발목을 잡는다. 때론 폴더 구조 개선만으로 리팩토링이 쉬워질수도 있다. 하지만 변경된 파일만 1200개… ㄷㄷㄷ

추가된 코드보다 삭제된 코드가 많으면 늘 옳다!

거대한 코드베이스를 이해하려면 때론 화가가 되어야한다. 그림을 그리고 나면 비로서 맥락이 보이기 시작한다. 하지만 이 변경도 파일 90개, 추가된 코드 천줄, 빠진 코드 천줄!

초반에 이런 작업이 많으면 비동기 코드 리뷰 자체가 불가능하다. 그리고 모두 신규 입사자. 거대 코드 베이스를 연구하는 유적 발굴단처럼 뭔가 발견하면 같이 공유하고 연구해야하는 운명공동체! 그렇게 매일 아침 팀원들과 화이팅 넘치는 수다로 시작해서 PR 리뷰받고 바로 수정해서 병합하기를 반복했다. 그렇게 몇달을 하다보니 자연스레 오전 일과는 팀원들과 함께 보내는 날들이 쌓이고 팀 문화가 되어 버렸다.

그러던 어느날 폴이 말했다. 매일 출근했던 전직장보다 이상하게 더 많이 대화하고 친밀감도 높고 매일 보는것 같다고.. 리모트 환경에서 매일 오전을 함께 코드 리뷰하면서 보낸 시간이 팀워크를 다지는 시간이 됐다. 리모트로 일했던 이전 회사에서도 친밀감을 위해 오전에 의도적인 티타임을 했는데 친밀감을 높이진 못했던거 같다. 그보다 어느 순간엔 시간 낭비라는 생각도들고 티타임은 흐지부지 사라졌다. 그런데 이번엔 좀 달랐다. 어떤 차이가 있는 것일까? 아마도 공통의 목적을 가진 목적 조직이라서 가능한게 아닐까? 서로 다른 프로젝트를 하는 기능조직에선 내 PR을 들고와서 같이 고민한다는게 쉽지는 않은 것 같다.

다시 심겨지는 잔디

매니저로 있었던 시기나 보안에 민감했던 회사에선 보통 엔터프라이즈 깃헙을 사용하기 때문에 개인 계정에 잔디 심기가 쉽지 않다. 퇴직하고 구직자로 전환되는 순간 이 잔디 심기가 마음에 좀 걸리는데 왜냐면 내가 누군가를 채용하는 면접관이었을 때 나는 항상 지원자의 블로그와 깃헙을 찾아보곤했다. 좋은 사람을 채용하려면 그만한 노력이 수반된다. 짧은 면접에 이사람의 장단점을 극적으로 뽑아내려면 면접관은 FBI가 되어야한다. 사전에 알수있는 정보가 많다면 미리 알고 들어가는게 좋다. 그래야 양질의 질문을 할수있기 때문이다. 지피지기면 백전 백승!

여하튼 이런 이유로 나는 이력서에 블로그와 깃헙 계정이 없는 지원자는 대부분 서류에서 탈락시켰다. 이제 그 화살이 내게 다시 돌아왔다. ㅎㅎㅎ 구직자는 열심히 블로깅하고 깃헙에 액티비티가 없는 이유를 방어해야한다. 내가 이런 글을 쓰고 있는 것도 다 이런 이유가 있는거다!

3년간 잔디 하나 없는 깨끗한 개인 정원

아무튼 나는 지난 2년 5개월을 충실히 개발자로 살았다. 빼곡한 잔디가 농부의 마음처럼 뿌듯하다. 심지어 이 잔디는 스쿼시 PR 로만 쌓은 커밋이라 자부심 하나를 더 한다.

그리고 나는 한 아이의 아빠로 충실한 삶을 살았다. 결혼하고 아이가 생기면 개발자에겐 큰 시련이 온다. 공부할 시간이 절대적으로 부족하기 때문이다. 재택으로 일 하다 보면 퇴근보다 아이의 하원이 빨라서 강제 육아 모드로 전환된다. 부모로부터 물려받은 성격이 있어서 직업윤리를 벗어나는 행동을 잘 하지 못한다. 그러다보니 하던일을 마무리하지 못하면 육퇴한 밤 11시에 업무로 다시 돌아가곤했다. 그러니까 잔디가 저렇게 금요일에서 토요일로 넘어가는 경계에도 심겨버린다. 특히 디버깅 중에 육아로 강제 전환되면 헤어나오질 못한다. 아이를 씻기면서도 머릿속에선 컴파일 중이다.

하루는 아이가 미열이 있어 어린이집을 보내지 않았다. 엄마도 5일간 자리를 비운 탓에 온전히 육아와 일을 병행해야하는 상황에 놓였다. 아빠와 노는게 좋은 우리 아이는 늘 그렇듯 아빠 책상 옆에 긴 의자를 하나 대놓고 종이 접기에 한창이다. 만 4살은 종이 접기를 잘 할 수 없는 나이다. 그러니 간간히 놀아달라 접어달라 인터럽트가 꾀나 집요하다. 회의하다말고 마이크를 끄고 버럭 화를 냈다. “기다려! 아빠 회의 좀 하자!” 시무룩해진 아이는 어느새 조용해진다. 그리고 나는 다시 돌아가 30분을 더 떠들고나서야 아이 생각에 옆을 본다. 세상 곤히 잠든 아이… ‘아!… 내가 무슨 부귀 영화를 누리겠다고 이 지랄을 하고 있나..’ 싶은 생각이 들 정도로 기다리다 지쳐 쭈구려 자고 있는 아이를 보면 명치 한구석에서 짠~~ 함이 몰려온다.

육아와 일을 병행하는 부모라면 엄마 아빠 할 것 없이 모두 이와 비슷한 경험을 했을 것이다. 그래서 나는 당분간 시대가 필요로 하는 개발자이길 포기했다. 육아와 업무만으로도 벅찬데, AI 시대에 LLM 공부를 어떻게 하나? 물론 나도 책은 사뒀지만 읽기가 참 어렵더라. 지속적인 학습이 되지 않는 상황이라면 도로묵이다. 과감히 투자를 포기한다. 한때 기업 강의를 꾸준히했던 덕에 여기 저기서 들어오던 강의 요청도 모두 거절했다. 나는 아이가 10살이 되기 전까진 뛰어난 개발자보다는 그냥 좀 더 성숙한 아빠가 되고자 한다.

개발자도 사람이다.

일정 수준에 도달한 이후 공부를 하지 않는 개발자에 대한 이야기가 한동안 돌았다. 내심 찔리긴 했지만 몰라! 나는 지금 그게 중요하지 않다. 아이가 지금 짠 한데 무슨 천자문 공부냐? 나는 그럴수록 더 다짐한다. 개발자이기 전에 성숙한 더 나은 사람이 되자.

그런데 재밌는건 시니어 개발자의 기준을 어떻게 가르나요? 라고 동료 개발자들에게 물어보면 코딩을 잘 한다거나 지속적인 학습을 하는 개발자라고 딱히 시니어라고 평가하지는 않았다. 오히려 그 외적인 모든 것을 기준삼는 경우가 많았다. 예를 들면 그 중 하나가 소통 능력인데 아이러니하게 아이와 대화하다보면 소통 능력과 인내심이 발달한다.

아이는 모든 것에 서툴고 실수 투성이다. 인내심도 어른 같지 않아서 징징대고 떼쓰기 일쑤다. 그런 아이를 보고 있노라면 나도 모르게 버럭 할때가 있다. 그럼 또 미안해서 사과하면 괜찮다며 이해한다고 나를 위로해준다. 이게 어른인지 애인지 가끔 헷갈린다. 한번은 어린이집에서 길을 건널때 손들고 건너야 한다고 배웠나보다. 나와 건널목을 걷는데 나에게 왜 손을 들지 않냐며 아빠도 어서 손을 들라고 성화를 내 길래… 일단은 손을 들어 건넌 후에 차분히 아이에게 설명했다.

“라일아, 길 건널때 왜 손 들고 가야하는지 알아?” / 쭈볏쭈볏 (내심 그 이유는 모르는것 같다) / “아빠가 설명 해줄께. 잘 들어봐! 라일이는 키가 작아서 운전하는 사람이 잘 못 봐. 그래서 키 작은 사람은 손을 들어줘야 운전자가 보고 멈출수있어. 그래서 아빠 어릴땐 큰 노란색 카드를 들고 다녔거든. 그런데 지금은 아빠가 키가 커서 노란색 카드도 필요 없고, 굳이 손들고 걷지 않아도 돼”

이날 이후 우리 아이는 내가 손을 들지 않아도 뭐라고하지 않는다. 하루는 아이에게 다시 질문했다.

“라일아, 길 건널때 왜 손들고 걸어야해?” / “어..왜냐면 내가 키가 작아서 운전하는 사람이 날 잘 못봐 그래서 손을 안들면 나를 쾅~하고 치고 갈수있어. 그래서 손드는 거야” / 덜덜덜… 물어본 내가 바보다. 이미 한방에 이해하고 있었다.

여하튼 나와 아내의 육아관은 아이에게 ‘너 자꾸 떼쓰면 망태기 할머니가 너 잡아간다’ 라는 과장된 이야기를 하지 않고, 정확한 이유를 설명하고 있는데 이게 습관이 되다보니 직장 생활에서도 이어진다. 평소에도 되짚는 질문을 많이 하는데 다행히 동료들도 내 질문을 공격적으로 받아들이진 않는것 같다. 되려 본질을 다시 고민해서 좋다고 피드백받아서 안심했다.

팀워크의 본질

육아 하면서 아이에게 배우는게 참 많다보니 사회생활에서 주니어와 시니어를 나누는 것 자체가 모순이라는 생각이든다. 시니어라고 항상 완벽하고 누군가를 이끌어야하는 존재도 아니고 주니어라고 항상 배워야하는 존재도 아니다. 우리는 모두 불완전한 존재다. 그러니까 팀으로 일한다.

7년전이었나? 넷플릭스 인재상이 한때 유행한적이 있었다. 모든 것에 유능한 만랩 개발자! 나 또한 이 인재상을 차용해 팀원들과 일대일 면담을 할 때 나만의 원통형 개발자 이론을 열심히 설파했던 적이 있었다. 원통형 개발자는 이런거다. 마치 축구 게임의 레이더 그래프처럼 슈팅, 스태미너, 패싱, 롱킥, 태클, 수비 등등 능력을 표현하는 요소들이 무수히 많고, 그 능력치가 만랩인 사람이 최고의 개발자라는 지론 그리고 그런 사람하고 일할때 나는 너무 좋았다. 그러니 너희들도 만랩이 되어라… 라고 설파한건 아니다.

7,800개의 Radar graph 이미지, 스톡 사진, 3D 오브젝트, 벡터 | Shutterstock

원통형 인재의 핵심은 “너만의 핵심 팩터를 스스로 결정하고 그 파이의 크기를 키워라” 라는게 나의 핵심 골자다. 왜냐면 축구팀에 메시만 존재하는건 아니니까. 위치에 따라 혹은 대항하는 팀에따라 다양한 능력의 선수들이 기용되기 때문이다. 다 각자 개성대로 쓸모가 있다.

어쨌든 이런 사고의 시작은 넷플릭스 만랩 개발자에서 영향을 받은건 사실이다. 그래서 팀을 상상할때도 만랩 개발자들이 있는 팀에서 일하고 싶은 욕망이 강했다. 이상적인 팀이란 그런 만랩 개발자가 모인 팀이라 생각했다. 그리고 그런 팀이라고 생각했던 회사를 다녔던 적도 있다.

그런데 이번 스테이지에서 생각이 바꼈다. 현실에서 팀원 개개인은 만랩일수없다. 그리고 생각해보니 만랩이라고 생각했던 팀에서 나는 혼자 일한 경험이 더 많다. 만랩 개발자는 굳이 팀으로 일할 필요가 없다. 오히려 만랩 개발자들이 모여서 불협화음을 낸 팀도 봤다.

팀을 만드는 이유는 개인이 불완전하기 때문이다. 불완전한 개인을 보완하는 수단으로 현실에선 팀으로 일한다. 나는 팀안에서 나의 부족함을 수혈받는 경험을 지난 2년 동안 무수히 많이했다. 대표적인 예시는 이런거다. 하루종일 혹은 밤새며 몇일간 버그와 씨름하고 있다. 그러다가 도저히 머리가 안돌아가서 다음날 아침에 팀원들에게 가져가면 여지없이 5분만에 해결되는 경험을 하게 된다. 또 이런 경험들이 쌓이면 팀안에서 안정감마저든다.

하루는 팀원들에게 이런 고백을 했다. “나는 시니어지만 시니어라고 모든걸 다 잘하진 않는다. 그래서 때론 불안한데, 우리 팀에서 나는 그 불안함이 없다. 내가 못풀면 내일 아침, 알란이나 폴이 분명 이 문제를 풀어줄꺼란 확신이든다.” 이런 안정감을 주는 팀은 내 커리어 안에서 없었던것 같다. 우리팀이 최고다! 작개는 3명인 개발팀이지만 크게는 목적 조직으로 7명인 팀 안에서 나의 이런 불완전함을 심심치 않게 고백한다. 언제 어디서 이런 팀원을 또 만날수있을까? 감사하고 또 감사한 일이다. 퇴직함으로써 이런 팀원들과 헤어지는건 분명 아쉬운 일이다. 그런데 뭐 헤어짐의 이유가 나의 퇴직은 아니니까. 아쉬워도 어쩔수없다.

내가 2년전 팀에 합류할때 왜 무너진 팀에 합류하냐면 나를 만류하던 지인들에게 무너진 팀을 재건하는게 내 전문이라고 호기롭게 얘기했던거 같다. 그런데 정작 팀 회고에선 어떻게 이런 팀원들을 만났는지 이건 운빨이라고… ㅎㅎㅎ

그래서 다음 스테이지에선 이게 운빨이 아님을 증명하고 싶다. 좋은 팀은 운빨일 수 없다. 좋은 팀은 의식을 가지고 만들어지는거다. 다시 한번 주문을 외울때가 되었다.

Optimization React rendering

랜더링 최적화는 어떻게 하나?

1차적으로 불필요한 랜더링을 먼저 막습니다. 불필요한 랜더링이란 화면에 반영되는 데이터가 동일하거나 다른 화면에 가려져 랜더링이 아예 필요 없는 경우를 말합니다.

Props로 넘겨지는 데이터가 동일 할때 React.memo 를 사용하는 방법은 두 가지가 있습니다.

1. 기본 탑재된 비교 알고리즘을 쓰는 경우

React.memo(컴포넌트)

기본 알고리즘을 쓰는 경우는 Props로 넘어오는 값들이 이뮤터블임을 전제로 합니다. 뮤터뷸 객체가 넘어오면 당연히 이 전략은 통하지 않습니다. 여기서 신경써야하는 것들이 바로 핸들러나 콜백 함수들인데, JS에서 함수는 뮤터뷸한 객체라서 useCallback 으로 일일이 감싸서 캐시(메모이제이)하지 않으면 효과가 없습니다.

그렇다고 항상 useCallback 쓸 필요도 없습니다. 캐시 비용도 비용이지만 매번 콜백 함수를 감싸는 귀차니즘도 만만치 않기 때문에 그 비용이 오히려 비효율일 경우가 많습니다. 이럴때는 비교 로직을 커스텀 하는게 더 효과적 입니다.

2. 비교 알고리즘을 커스텀 하는 경우

React.memo(컴포넌트, 비교로직함수)

비교로직을 커스텀 할경우엔 콜백함수를 useCallback 으로 넘기든 넘기지 않든 랜더링에 필요한 요소들만 선택적으로 찾아서 랜더링을 결정합니다. 따라서 메모된 값을 쓰고 싶을 경우엔 true, 새로 랜더링을 할 경우엔 false를 비교 함수에서 반환합니다.

최적화 우선순위

모든 컴포넌트를 다 useCallback 으로 감싸거나 React.memo로 감쌀 필요는 없습니다. 병목 지점을 먼저 찾고 필요한 곳 위주로 처리하면 됩니다.

예를들어 아래와 같은 구조에서 최상위 Root 컴포넌트가 랜더링되면 나머지 A~F 컴포넌트가 줄줄이 랜더링 됩니다. 이때 A가 한번 랜더링 되면 F 같은 리스트 아이템들은 수없이 많이 랜더링 됩니다.

  ---Root
      |--- A
           |-- B ( List )
               |-- F
               |-- ... N 개의 F 
               |-- F
           |-- C
      |-- D

즉, 위와 같은 구조에서 F 컴포넌트가 제일 먼저 최적화 대상이 됩니다.

극단적인 랜더링 최적화 (한번만 그리기)

가끔은 컴포넌트를 딱 한번만 그리는 경우도 있습니다. 예를 들면 고정형 네비게이션바 같은 경우엔 Props를 주입받지만 화면을 그리는 요소에 영향을 주지 않기 때문에 이런경우엔 극단적인 선택을 할수있습니다.

export default memo(NavigationBar, () => true);

클로저 변수와 함께 콜백 함수 메모하기

   1번
   const 핸들러함수 = useCallback(()=>{
         const currentItem = items[currentIndex];
      //  currentItem 처리
   },[currentIndex])

    2번 
    const 핸들러함수 = (currentItem) => {
         // currentItem 처리....
    }   

    <List>
        {for item in items 
            return <ListItem item={item} onPress={핸들러함수}>
         }
     <List>

1번처럼 useCallback 함수를 사용해 클로저 변수를 함께 패키징 할 경우 핸들러 함수는 currentIndex 값에 따라 매번 다른 함수가 만들어집니다.

그리고 이것을 <ListItem> 컴포넌트의 프로퍼티로 넘기기 때문에 <ListItem> 컴포넌트의 메모 전략은 매우 단순하게 가져갈 수 있습니다.

export default memo(ListItem);

물론 currentIndex 갯수 만큼 메모하는건 그만큼 약점입니다.

반대로 useCallback 을 쓰지 않을 경우에는 당연히 ListItem의 최적화 전략이 달라져야합니다.

export default memo(ListItem, (prev: Props, next: Props) => {
  return prev.language === next.language;
});

그리고 2번처럼 핸들러 함수에 넘겨받은 값을 그대로 같이 반환해야합니다.

function ListItem(props){
  const { item } = props;

  return( 
     <TouchableOpacity onPress={() => onPress(item)}>
       // 블라블라...
     </TouchableOpacity>
  )
}

주의! 최적화 전략은 케바케라서 상황에 따라 적절히 취사 선택하려면 잘 알아야한다. 암튼 간만에 회사 일 하다가 긴 PR을 올려서 그대로 블로그에 옮겨 왔음.