App Screenshot

iOS 앱 Tiny Tip Calculator를 만들었다.

계기

매번 식사를 밖에서 할 때마다 팁을 계산하는 모습을 보고 간편한 팁 계산기가 있으면 좋겠다고 생각했다. 그래서 앱스토어에서 받으려고 검색했는데 수많은 팁 계산기가 다음 부류였다.

  • 광고가 지나치게 많아서 사용성을 크게 해침
  • 결과를 보기까지 인터렉션이 너무 많이 필요
  • 그냥 안 이쁨

어떤 앱은 세 가지 모두에 해당했다. 그래서 간단한 앱을 하나 만들기로 했다.

도구 선정

집에 있는 모든 사람이 아이폰을 사용하고 있어서 iOS 앱을 만들기로 했고 무엇으로 개발할지 고민했다.

  • Swift는 일단 네이티브니 성능도 좋고 원하는 만큼 뜯어고칠 수 있겠지만 익숙하지 않았다. 물론 만들면서 배우는 것만큼 학습에 좋은 방식은 없지만 원하는 결과물을 만들어 내는 데 집중하고 싶었다.
  • React Native도 고려했다. React Native의 툴링도 좋고, 성능도 마음에 들고 예전보다 확실히 리소스가 많았다. expo.io도 멋지다.
  • Ionic은 Cordova와 Angular를 잘 섞은 프레임워크였는데 아무래도 웹앱이라서 성능에 대한 걱정이 들었다. 그래도 문서도 정리가 잘 된 편이었고 네이티브 기능을 쉽게 사용할 수 있도록 플러그인도 많이 제공했다.

React도 계속 공부하고 써보려고 하고 있지만, 여전히 Angular가 익숙한 데다 생각보다 Ionic의 성능이 좋아서 Ionic으로 선택했다. React에 좀 더 익숙하다면 React Native를 고민 없이 선택했을 것이다. 결과물을 빠르게 보겠다는 생각 탓에 편향적인 결정을 내렸다.

개발 목표

개발하기 전에 다음 목표를 정했다.

  • 내장 키보드 말고 키패드 직접 만들 것, 계산기처럼 모든 기능이 보이도록.
  • 입력 횟수를 최소로 하기.
  • 팁 비율을 목록으로 보여줌. 비율은 프리셋 설정할 수 있도록.
  • 이쁘고 깔끔하게, 슬라이드 같은 것도 넣지 말고. 테마도 지원하면 좋겠음.
  • 앱처럼 보이도록!

개발 계획

프로젝트를 생성할 때 정도만 문서를 봤고 Angular는 이미 익숙해서 평소 작업하듯 만들었다. 로직은 별문제 없이 만들었지만, 스타일에 수고가 많이 들었다.

전체적으로 페이지 수는 얼마 되지 않았다.

  • 계산 페이지
    • (계산 모드)
    • (결과 모드)
  • 설정 목록
    • 통화 및 팁 비율 설정
    • 테마 설정
    • 소개 페이지

Angular는 각 컴포넌트나 디렉티브, 모듈의 선언적 관리가 전체적인 코드 구성에 정말 편리했다. TypeScript와 함께 궁합도 너무 좋았다. 프레임워크 답게 전체적인 만듦새나 구조는 Angular가 확실히 더 이해하기 좋게 느껴진다.

앱에서 필요한 부분은 모두 ionic에서 제공하는 플러그인으로 충분했다. Storage 등도 이미 다 고수준으로 제공하고 있는 데다 angular에서 손쉽게 의존성 주입으로 활용할 수 있었다.

Good & Bad

웹과 다른 흐름의 도구를 제대로 만들어서 결과를 낸 것은 처음이었다. 이 과정에서 좋았던 점은 다음 같았다.

  • 익숙한 도구를 선택해서 빠른 결과를 냈다. 첫 PoC를 만드는 데 하루 걸렸고 전체적으로 코드를 정리하고 작성하는데 3일 정도 사용했다. 테마 구현을 가장 고민했었는데 의외로 손쉽게 해결했다.
  • 다른 앱의 리뷰를 찾아보고 새로운 유즈케이스를 찾아 기능을 추가했다. 리뷰 중에 세금 불포함 계산 방식이 필요하다는 이야기가 반복되었지만 지원하는 앱이 거의 없었다. 그래서 해당 기능을 첫 릴리즈 이후 추가해서 배포했다.
  • 파워 유저(이자 아내)에게 빠른 피드백을 받을 수 있었다. 실제로 앱을 사용하는 과정도 옆에서 볼 수 있었던 점도 새로운 경험이었다. 버튼의 위치나 세부적인 기능에 대한 조언은 앱에 반영되었다.

아쉬운 점도 있었다.

  • 익숙한 도구를 사용해서 기술 배경에서 크게 도전적인 프로젝트는 아니었다. 게다가 네이티브 환경이 아니라서 이쁘지 않은 부분이 계속 눈에 걸렸다. 예를 들면 스크롤바가 화면 밖에 걸쳐 있다든지 하는 부분은 하이브리드앱에서 흔히 겪는 문제다. 이런 부분이 크게 사용성을 해치는 상황은 아니지만 안 이쁜 건 계속 거슬렸다.
  • 테스트가 미흡했다. Ionic과 Angular 모두 테스트에 유리한 환경을 기본적으로 제공하는데도 테스트를 부지런히 작성하지 않았다. 빠르게 작성하는 데는 성공했지만 좋은 품질을 유지하고 작성하는 일에는 소홀했다.
  • 수익성을 고려하지 않았고 프로모션도 하지 않았다. 다른 앱 리뷰에서 광고 얘기를 보면서 아예 광고를 생각하지 않았다. 지금이야 크게 나쁜 결정이라는 생각은 들지 않지만, 애플 개발자 계정을 연장할 때면 왜 이런 결정을 했을까 생각 들 것 같다.

결과

이미 많은 앱이 존재해서 검색 목록 위로 올라가는 일조차 쉽지 않지만 어떤 방식으로든 결과물을 빠르게 냈다는 점에 즐거웠다. 그리고 작은 앱이더라도 옆에서 부지런히 쓰는 사용자가 있어서 재미있었다. 다음 또 앱을 만든다면 어떤 점을 미리 고려해야 하는지도 배웠다.


트러블 슈팅

iOS에서 overflow로 생성한 스크롤 부드럽게(?) 하기

스타일에서 overflow 프로퍼티를 적용하면 웹 페이지 내에 스크롤을 넣을 수 있다. 그런데 iOS에서는 그 스크롤을 써보면 일반적으로 경험할 수 있는 스크롤처럼 부드럽지 않고 뻑뻑하게 움직인다. 이 동작을 iOS의 기본 동작처럼 바꾸려면 다음 스타일을 추가로 넣어야 한다.

--webkit-overflow-scrolling: touch;

최근 모델의 notch 해결하기

이 스타일은 Safari만 지원한다.

먼저 웹뷰의 페이지에서는 기본적으로 상단의 스테이터스바를 안전 영역으로 처리한다. 그래서 고정 영역을 예로 들어 top: 0로 설정해도 스테이터스바 바로 아래에 자리를 잡는다. 화면 전체를 웹 영역에 넣고 처리하고 싶은 경우에는 viewport-fit을 viewport에 추가해야 한다.

viewport-fit은 다음 3가지 값을 지원한다. csswg 문서에 그림을 잘 그려놨다.

  • auto: 기본 값으로 아무 동작하지 않는다.
  • contain: 웹뷰 영역이 잘리지 않도록 디바이스 영역에 맞춘다.
  • cover: 디바이스 영역에 빈틈이 없도록 웹뷰 영역을 맞춘다.
<meta name="viewport" content="viewport-fit=cover, ..." />

이제 화면 전체 영역을 사용할 수 있게 되었다. 이제 스테이터스바 영역만큼 밀어야 할 필요가 생긴다.

header.global {
    padding-top: 20px;
}

하지만 이렇게 고정값을 넣으면 노치가 없는 기기에서 높이가 이상해진다. 이런 상황에서는 safe-area-inset-* 상수를 사용하면 된다.

heaader.global {
    padding-top: constant(safe-area-inset-top); /* iOS 11.0 */
    padding-top: env(safe-area-inset-top); /* iOS 11+ */
}

constraint()는 없어질 예정이며 이후로는 env()를 사용하면 되겠다.

CSS Grid

고정된 화면을 기준으로 각 컴포넌트를 배치하는데 css grid가 정말 편했다. vw, vhcalc()를 함께 쓰면 어떤 컴포넌트든 원하는 위치에 놓을 수 있었다.

position: sticky

stickyposition 프로퍼티에 사용하면 필요한 요소를 고정으로 띄울 수 있다. 예전엔 위치 계산해서 fixed를 직접 설정해줬어야 했는데 손쉽게 구현할 수 있다.

다만 iOS에서 sticky에 배경을 지정해도 위에 1px 정도 아래 엘리먼트가 보이는 문제가 있었다. transform: translateY(-1px)를 추가해서 약간 비틀어서 해결되었고 GPU 가속이 동작해선지 약간 버벅이던 동작도 없어졌다.

:host-context()

하위 컴포넌트가 상위 컴포넌트의 스타일에 따라 스타일을 제어해야 할 때가 있는데 여기에 :host-context()를 사용할 수 있다. [theme]과 같은 디렉티브를 사용해서 부모 엘리먼트에 클래스를 추가하도록 했다면 :host-context()로 해당 클래스를 추적하는 것이 가능하다.

// hello.theme.scss
:host-context(.theme--hello) {
    * {
        font-family: 'Arial Rounded MT Bold', sans-serif;
    }

    header {
        color: #ff00c3;
    }
    // ...
}

현재는 이렇게 만든 각 테마 scss를 한 곳에서 모두 불러오는 구조로 되어 있다. 이 구현을 다시 한다면 중간에 테마를 제어하는 컴포넌트를 ViewEncapsulation.None로 놓고 테마 스타일을 동적으로 불러오도록 처리하고 싶다.

오버 스크롤 끄기

하이브리드앱을 가장 하이브리드앱처럼 보이게 하는 동작 중 하나가 오버 스크롤이다. 이 동작으로 전체 레이아웃이 고정된 부분 없이 움직이면 앱처럼 느껴지지 않는다. Cordova에서는 다음 속성을 config.xml에 추가하면 이 문제를 해결할 수 있다.

<!--config.xml in the project-->
<preference name="DisallowOverscroll" value="true" />

Version API 만들기

최신 버전을 확인하고 새 버전이 있다면 업데이트를 하도록 작은 버튼을 띄워주고 싶었다. 예전엔 블로그용 서버가 있어서 코드를 작성해서 올리면 되었겠지만 이제는 정적 블로그를 사용하고 있어서 아무래도 제한되는 부분이 있었다. 요구 사항은 이랬다.

  • 적어도 response header를 제어할 수 있어야 함
  • 반환값은 하드 코딩이어도 큰 상관 없음, 디비 연동도 필요 없음
  • 비용으로 가장 저렴할 것 (연 5불 이하)
  • 인증 필요 없음
  • https 지원
  • 관리는 최소로

그래서 Azure Functions를 선택하게 되었다.

module.exports = async function (context) {
    context.res = {
        headers: {
            'Content-Type': 'application/json',
        },
        body: {
            version: '1.1.4',
            tagline: 'New Theme: Mono',
            link: 'https://itunes.apple.com/app/tiny-tip-calculator/id1448227957?mt=8'
        }
    };
};

아쉽게도 현재 netlify에 물려 있는 도메인을 사용해서는 API 주소를 https로 사용할 수 없었다. Azure의 앱서비스 내로 도메인을 가져와야만 A 레코드로 사용할 수 있었고 CNAME은 https의 인증서에 문제가 있다고 접근이 되질 않았다. 큰 기능의 API도 아닌 탓에 그냥 기본으로 제공하는 azurewebsites.net의 서브 도메인을 사용했다.

Azure Functions의 비용은 앱 서비스 플랜과 종량제 플랜 중 하나를 고를 수 있는데 앱 서비스 플랜의 경우는 앱 서비스를 사용해서 function app을 실행하는 방식으로 앱 서비스만큼 비용을 내야 하고 종량제는 쓰는 만큼 비용을 지불하는 방식이다. 내 경우에는 크게 사용량이 많지 않을 것이라는 판단에서 종량제 플랜을 선택했다.

하지만 종량제 플랜에서는 Cold start가 너무 느렸다. 앱서비스 플랜은 인스턴스를 계속 띄우고 있기 때문에 항상 빠른 응답을 받을 수 있다. 하지만 종량제 플랜에서는 Function app 호출이 특정 시간 동안 없을 땐 해당 코드가 구동되는 인스턴스를 없엤다가 호출이 있을 때 인스턴스를 다시 생성해서 코드를 올려 구동한다. 그래서 인스턴스가 올라와 있는 상황에서 호출하면 warm start로 응답이 100ms 미만으로 매우 빠르지만, cold start는 인스턴스를 올리는 것부터 시작하기 때문에 더 오랜 시간이 걸렸다. 내 경우에는 코드가 의존성도 없고 매우 단순한데도 8초에서 22초까지 걸렸다. 비용은 코드를 실행해서 결과를 반환하기까지 시간에 대해서만 청구되기 때문에 비용적으로 문제는 없었다.

현재 사용량이 상당히 적은 상태인데 이 문제 때문에 앱 서비스 플랜을 사용하고 싶지는 않았다. 그래서 해결 방법을 찾아보니 그냥 계속 살아 있도록 function을 호출하는 방법이 있었다. 그래서 5분 간격으로 function app을 실행하도록 Timer 트리거를 추가했다.

{
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 */5 * * * *"
    }
  ]
}
module.exports = async function () {
    console.log('health checked');
};

만약 사용량이 많아지면 Azure Functions는 알아서 스케일링을 수행한다. 스케일링을 수행하면 새 인스턴스가 생성되면서 cold start가 다시 발생할 수 있지만, 현재는 두 API 모두 큰 자원을 소모하지 않고 있고 사용량도 적기 때문에 계속 warm start를 유지할 수 있었다. 만약 다른 인스턴스가 올라가서 다시 cold start가 감지된다면 그때는 앱 서비스 플랜으로 변경할 생각이다.

현재까지 비용 예측은 월간 $0.25 정도고 이조차도 Functions 호출 비용보다 코드가 저장된 공간인 스토리지의 비용이 대부분을 차지하고 있다.

빌드와 배포

빌드와 배포 과정에서 문제가 되었던 부분이 몇 있었는데 검색으로 쉽게 해소했다.

  • Xcode의 모던 빌드 시스템을 사용하면 문제가 생긴다. 빌드를 생성할 때 다음 플래그를 추가한다.
    $ ionic cordova build ios -- --buildFlag="-UseModernBuildSystem=0"
    
  • Xcode에서 **File > Project Settings...**에 들어가서 Build System을 Legacy Build System으로 변경한다.
  • Signing에 문제가 있다고 나오면 Automatically manage signing의 체크 박스를 해제하고 Xcode를 다시 실행해서 다시 체크한다.
  • 일반적인 암호화 외의 기능을 사용하면 앱 배포에 추가적인 절차가 필요하다. 하지만 단순히 API 호출에 HTTPS를 사용하거나 인증 절차에 사용하는 경우에는 예외에 해당한다. info.plistITSAppUsesNonExemptEncryptionNO로 설정한다.

이 글은 April WenselTech has a Toxic Tone Problem — Let’s Fix It! 번역입니다.


2023년 5월 추가: 잇창명님이 이 글을 재번역해주셨습니다. 해당 글은 잇창명님 웹페이지에서 확인하실 수 있습니다.


기술 업계의 독성 말투 문제, 고칩시다!

의사소통에 관해서, 특히 엔지니어가 연관된 경우라면 기술 업계에서 독성 말투 문제가 존재합니다. 저는 이 문제를 지난날 동안 주변에서 겪었기 때문에 알기도 하지만, 저도 이런 순간을 마주했을 때는 그 문제에 기여를 했었기 때문입니다.

먼저 말투(tone)는 "누군가 단어를 말하거나 쓸 때 표현되는 태도"라는 의미에서, 독성(toxic)은 "조직의 자산 또는 구성원을 포함해서 그 조직에 위해를 주는 것"이란 의미에서 사용하고 있습니다.

소프트웨어 엔지니어는 일반적으로 뛰어난 커뮤니케이션 스킬을 갖고 있지 않다고 얘기합니다. 이런 경향은 여러 이유로 설명되기도 합니다. 전문 지식을 갖고 있기 때문에, 인간관계보다 컴퓨터와 더 많은 시간을 썼기 때문에, 전통적으로 성격이 프로그래밍에 가까워서 등으로 말이죠. 하지만 이런 대부분의 잠재적 이유는 문제점이 있으며 특히 마지막 이유는 고정관념이기도 합니다. 게다가 엔지니어가 이런 문제점을 핑계로 주변 사람들에게 멀쩡한 사람처럼 행동하지 않을 구실로 삼을 수 없다는 점입니다.

엔지니어의 부족한 커뮤니케이션은 새로운 현상이 아닙니다. 제럴드 와인버그는 1971년에 프로그래밍 심리학에서 다음처럼 적었습니다.

프로그래밍에서는 엄청나게 똑똑할지 몰라도 자신의 지적 능력을 사용해서 자신의 사회적 행동이나 대화 방식을 고칠 수 있을 정도로 똑똑하지 않을 수 있습니다.

이런 점은 단순히 팀 생산성을 망치는 일에 그치지 않습니다. 커뮤니케이션 문제가 기술을 공부하는 사람들을 낙담하게 하며 사회적 활동에 참여하는 기존 엔지니어의 의지까지도 꺾게 됩니다. 여기서 사회적 활동이란 스택오버플로우에 질문을 올리고 답변을 달거나 오픈 소스에 기여하는 활동을 예로 들 수 있습니다. 그러므로 프로그래밍을 둘러싼 커뮤니케이션을 향상하는 일은 더 포괄적인 개발 커뮤니티를 만드는 일에 필수적이라 할 수 있습니다.

이 문제를 지적하기 전에 먼저 이해를 제대로 하기 위해 살펴봐야 합니다. 저는 그동안에 이 말투 문제를 지켜보면서 세 가지 공통분모를 찾았습니다. 물론 이 사항은 완벽한 목록이라고 말할 순 없지만, 엔지니어만 이런 커뮤니케이션 문제를 겪는 사람이 아니라는 점을 확인할 수 있을 겁니다.

이런 독성 말투는 일반적으로 거들먹거리는 말투, 기계적 말투, 또는 비관적인 말투로 나타나기도 합니다. 어떤 경우에는 달콤한 독성 말투 비빔밥이라도 되는 것처럼 이 세 가지를 모두 섞어 놓은 때도 있습니다.

거들먹거리는 말투

어떤 기술을 습득하게 된다면 아마 주변 사람들에게는 부족한 전문 지식을 갖게 될 겁니다. 나쁘게도 새로운 기술을 습득한다고 해서 그 기술이 없는 사람과 효과적으로 의사소통하는 능력까지 함께 따라오지 않습니다. 다른 사람과 대화하게 될 때 당신이 아는 만큼 알지 못하는 사람에 공감하는 능력이 부족하다면 거들먹거리는 녀석으로 보일 뿐입니다.

지미 펄론이 세러데이 나이트 라이브에서 했던 닉 번즈: 당신 회사의 컴퓨터 담당자를 본 적이 있나요? 닉은 따지고 보면 허구의 IT 전문가며 실제 세계의 프로그래머가 아니긴 하지만 극에서 묘사된 그의 행동은 놀랍게도 기술 업계에서 만연하게 볼 수 있던 거들먹거리는 말투를 명확하게 보여주고 있습니다.

저는 동료들이 다른 엔지니어나 인터뷰 대상자를 "종이봉투 뚫고 나올 간단한 방법조차도 프로그래밍하지 못하는" 그런 "멍청이"라고 부르는 경우를 수 년간 들어왔습니다. 초급 엔지니어가 질문했다고 눈을 부라리는 경우도 봤습니다. 부트캠프 졸업생이나 스스로 공부해서 프로그래머가 된 사람들을 평가하는 지적도 들은 적이 있습니다.

또한 많은 엔지니어가 말하는 방식이라든지 이들이 마케팅, 영업, 제품과 고객 응대에서 일하는 직원에 대해 언급하는 경우를 봤을 겁니다. 엔지니어가 누군가를 "비개발자 (non-technical)"라고 언급했다면 그 사람들의 업무를 뭉개버리는 것이나 마찬가지입니다. (다른 얘기지만 저는 이 단어를 더는 사용하지 말아야 한다고 봅니다)

이런 거들먹거리는 태도는 항상 노골적으로 드러나지 않습니다. 다음 예제는 조금 교묘합니다. 누군가 스택오버플로우에 남긴 질문인데 온라인에서 한참 검색하기도 했지만, 이해에 어려움이 있어서 옵저버옵저버블 의 차이점이 무엇인지 물어봤습니다. 다음 답변은 100회 이상의 투표를 받은 답변입니다.

읽기 좋게 거들먹거리는 부분을 가져왔습니다.

난 이 내용을 어떻게 더 평범한 영어로 설명할 수 있을지 모르겠네요.

지금 위 내용에 정의가 있습니다. 딱 두 문장입니다. 10번 읽으면 더는 이해할 것도 없을 것 같은데요.

질문에 답변하는 사람은 용어의 차이가 명확해서 어떻게 이걸 이해하기 위해 더 "평범한 영어"로 설명해야 하는지 상상조차 하지 못하고 있습니다. 이 질문을 남긴 사람은 이 답변으로 어떤 기분이 들었을지 궁금하게 합니다. 질문자가 이 답변에서 응원을 받은 기분이거나 답을 읽고 더 자신감을 느끼게 되었을 거라고는 상상되질 않습니다. 이 문답이 충격적이라는 점을 트위터에서도 마주할 수 있었습니다.

운 좋게도 누군가 커뮤니티에서 답변을 수정할 수 있는 사람도 트위터 토론을 봤는지 거들먹거리는 서두를 지워서 유용한 답변으로 바꿔놨습니다. 커뮤니티 구성원 모두가 친절함에도 가치를 둬서 투표를 따라 줬더라면 얼마나 좋았을까요.

근본적으로 어떤 경우든 거들먹거리는 말투 뒤에 숨어 있는 태도는 "내 주변에 있는 사람들은 다 모르지만 나는 알고 있다. 내가 우월하기 때문에 그들을 굳이 존중할 필요가 없다."와 비슷할 겁니다.

이런 태도는 위험합니다. 죄와 벌에 나오는 라스콜니코프의 "초인" 콤플렉스를 연상하게 합니다. 이 사람은 스스로를 다수인간 위에 있는 법적 존재인 소수인간이라 여기고 다른 사람을 살해하기에 이릅니다.

물론 이처럼 극단적인 경우를 다루지는 않지만, 여전히 이런 사람은 우리 개발팀에 있는 것은 건강하지 않습니다. RailsBridge를 창업한 사라 메이는 이렇게 말했습니다.

직원의 보이지 않고 조용한 반사회적 행동에서 비용이 발생합니다. 그런 사람들의 비범한 생산성보다 정! 말! 큰 비용을 지출하게 합니다.

이에 대한 해결책은 무엇일까요?

평화적인 대안

먼저 대표직에 있다면 거만하거나 거들먹거리거나 공격적인 행동에 대해 보상하는 일을 중단해야 합니다. 개념을 진정으로 숙련했다면 경험이 적은 사람에게 더 간단한 용어를 사용해서 개념을 설명할 수 있어야 합니다. 이런 행동이 보상을 받아야 하는 행동입니다.

개인 수준에서는 무얼 할 수 있을까요?

동료가 도움을 받고 싶어서 하는 질문에서 본인이 잘 아는 분야라고 생각한다면 스스로 무언가 거들먹거리는 내용을 말할 수 있다는 점을 인지하고 있어야 합니다. 또한 내가 이런 답변을 누군가에게 들었을 때 어떤 기분이 들까 하고 스스로에게 물어봐야 합니다. 사용하는 단어가 듣는 상대의 기분을 상하게 하나요 아니면 좋게 하나요? 정말 그 사람보다 더 잘 알고 있는 게 맞나요? 더 알고 있다는 점이 상대에게 예의 없게 행동해야 한다는 의미인가요? 상대방도 당신이 모르는 부분을 더 잘 알 수도 있지 않을까요? 단순히 동일한 문제를 다른 관점에서 보고 있는 것은 아닐까요?

당신이 대화하고 있는 사람이 정말로 해당 주제에 대해 무지하다고 가정해봅시다. 상대방이 문제를 이해하는데 얼마나 어려움을 겪을지 상상해봅니다. 이해하는 동안 겪는 고통에 동정심을 갖고 그 어려움을 최소화할 수 있도록 하려면 어떻게 도울 수 있을지 살펴봅시다. 자신이 이 주제에 대해 처음으로 배우던 당시를 생각해본다면 어떻게 알려줘야 하는지 생각하는데 도움 될 겁니다. 어떤 점이 도움이 되었습니까? 다음을 고려해보세요.

  1. 어떤 질문들을 하고 있었고 가장 유용한 답변은 어떤 답변이었습니까?
  2. 어떤 자료가 이 문제를 이해하는데 있어 추가적인 관점을 제공할 수 있습니까?

또한 자기 자신에게 인정을 베풀기 바랍니다. 이 사람을 도와주는 일이 에너지를 소비하나요? 만약 그렇다면 좀 쉬는 것이 낫지 않을까요? 도와줄 수 있는 다른 사람은 없나요? 이 문제를 다루는 데 도움이 되는 연관 자료가 있나요? (주의하세요: "대신 구글링 해줄게" 링크를 보내는 일은 일반적으로 거들먹거리는 거나 다름없습니다. 상대방이 농담으로 받아들일 것이라는 확신이 있는 것이 아니라면 말입니다.)

스스로 이런 행동을 한다면 당신의 동기(motivation)를 확인해보세요. 사실, 이 지식에 대해 약간 불안전하게 느껴서 그런 행동을 보일 가능성이 있지 않나요? 자신의 자아를 방어하기 위해서 거들먹거려야겠다는 유혹을 받는 것은 아닌가요? 정말 이런 문제로 그렇다면 스스로를 채찍질하지 않기 바랍니다. 기술 산업에 있는 면접과 직원 보상 시스템은 주로 부풀려진 자아를 선호하기 때문에 아마도 이런 태도가 당신의 잘못만은 아닐 겁니다. 그러므로 조금의 자기 성찰로 무언가 공격적인 말을 하는 것을 방지할 수 있을 것입니다.

거들먹거리는 응답 대신에 연민이 있는 응답을 할 수 있다면 듣는 상대방도 기분 상하지 않아 당신과 함께 일하기 즐겁게 여길 것입니다. 올리비아 폭스 카반의 저서 The Carisma Myth(카리스마, 상대를 따뜻하게 사로잡는 힘)에서 이런 이야기가 나옵니다.

1886년, 한 여성이 두 명의 영국 총리 후보와 각각 저녁 식사를 했습니다. 이 여성이 저녁 식사에서 언론에 다음과 같이 말했습니다.

"글래드스턴 씨와 저녁 식사를 한 후에는 그가 영국에서 가장 현명한 사람이라고 생각했습니다. 디즈레일리 씨와 저녁 식사를 한 후에는 제가 영국에서 가장 현명한 사람이라는 생각이 들었습니다.

말할 필요도 없이 궁극적으로 디즈레일리가 선거에서 승리했습니다. 어떤 타입의 사람이 되고 싶은가요? 거들먹거리며 당신의 우월함을 자랑하는 사람이 되고 싶은가요? 아니면 다른 사람들의 가능성을 일깨우는 데 도움을 주는 사람이 되고 싶은가요? 단순하게, 당신은 어떤 타입의 사람과 일을 하고 싶은가요?

이 주제에 대해 더 알고 싶다면 책 The No A**hole Rule(또라이 직)을 확인해보세요.

기계적 말투

"안녕 패트, 네 사촌이 죽었다."

이 메시지는 제가 사랑하는 할머니가 제 유년 시절에 자동 응답기에 남긴 메시지입니다. 목소리에 억양도 없었고 요점을 전달하는데 감정적 수사 여구도 없이 단순히 효율적인 사실만 전달하는 말이었습니다. 저는 할머니의 직선적인 태도를 물려받았다고 생각합니다.

아쉽게도 모두가 감정 없는 직선적인 태도를 좋아하지 않습니다. 앞서 이야기한 것과 같이 저 또한 기술 분야의 독성 말투 문제에 기여를 했는데 이 로봇 같이 행동하는 문제가 바로 제 약점이기도 합니다.

엔지니어로서 컴퓨터를 다루면서 일반적으로 주의 깊게 사람의 감정을 고려하는 것처럼 행동하지 않습니다. 컴퓨터에게 매우 직선적으로 무엇을 할지 이야기하면 컴퓨터는 명령을 처리합니다. 아름답게 효율적이고 논리적인 흐름입니다. 아마 이런 특징이 많은 사람으로 하여금 엔지니어링에 첫눈에 반하고 빠지게 하는 그런 부분일 겁니다.

좋든 나쁘든 간에 컴퓨터와 사람은 동일하지 않습니다. 우리는 감정이 있습니다. 상대가 어떻게 받아들일지 고려하지 않고 사실을 직접 공유할 수 없습니다. 제 말은 물론 그렇게 직접 공유할 수 있긴 하겠지만 그러면 당신에게 "거칠다" 혹은 "직설적"이라는 이름표가 붙게 됩니다. (저를 믿으세요.) 인간에게 기계에 말하듯 한다면 사람들 대부분은 당신과 일하는 것을 좋아하지 않을 겁니다.

저는 제 할머니에게 감사하기도 합니다. 저는 이런 직선적인 태도가 엔지니어에게는 자산이 될 수 있다고 항상 생각하기 때문입니다. (사실 엔지니어뿐만 아니라 모두에게 말입니다.) 하지만 말을 듣는 청중을 주의 깊게 생각하는 일, 어떻게 말을 전달해야 하는지 그 모양을 다듬는 일이 중요하다는 점에 저는 동의하게 되었습니다.

중대한 순간에 웹사이트가 닫히게 되었다고 가정해봅시다. 지금 문제를 해결하고 있다는 확신을 보여주지 않고 직설적으로 이 소식만 전달한다면 아마 회사에서 가장 인기 있는 사람이 될 수는 없을 겁니다. 이런 종류의 뉴스도 기계적인 말투로 전달한다면 당신의 고객과 제대로 공감대가 형성될 수 있을지도 의문입니다. 또한 침착함을 유지하는 것도 중요하지만 최소한 고객이 어떤 영향을 미치는지 인지하는 것도 중요합니다.

기계적 말투는 피드백을 전달하는 과정에서도 문제입니다. 직접적인 피드백을 좋아한다고 여러 차례 언급한 디자이너와 함께 협업한다고 상상해봅시다. 이 디자이너는 지금 함께 작업하고 있는 애완동물 관리 앱의 가장 최근 목업(mockup)에 대해 조언을 구했습니다. 사용자의 애완동물 정보를 볼 수 있는 상세 페이지를 만들고 있었습니다. 이 목업에는 디자이너의 고양이, 닐에 대한 통계가 표시되고 있습니다. 페이지 상단에는 "당신" 이라는 제목이 붙어 있습니다. 대략 다음 같은 화면이지만 훨씬 멋지다고 생각해봅시다.

이 디자인을 본 후에 시나리오를 논리적으로 생각해보면 이렇게 말하게 될 겁니다. "이 화면의 제목은 애완동물에 대한 하위 페이지라면 적어도 '당신의 고양이' 또는 '당신의 애완동물' 아니면 '닐'이 되어야 하지 않나요?" 논리적이기도 하고 제목이 페이지 가장 위에 있으니 가장 먼저 보이기도 하니 이런 지적을 가장 먼저 할 수 있을 겁니다.

이제 디자이너와 좋은 관계가 있거나, 디자이너가 스스로 능력에 자신감이 있거나, 기분이 좋은 날이거나, 정말 앞서 말한 것처럼 직접적인 피드백을 좋아하는 사람이라면 디자이너는 오류를 지적한 점에 고마워하고 제목을 갱신하는데 동의할 겁니다. 아니면 왜 여기서는 이런 제목이 맞는지 기쁘게 설명할 수도 있겠습니다.

하지만 모든 상황이 잘 맞아서 돌아가지 않았다고 생각해봅시다. 먼저 디자이너가 그동안 디자인을 하면서 공들인 부분을 알아주지 않고 어떤 부분에서도 이 디자인의 장점을 언급하지 않으며 가장 먼저 문제를 딱 집어내서 그다지 기분이 좋지 않을 수도 있습니다.

스스로는 효율적이라고 생각할 수 있겠습니다. 왜 디자인에서 올바른 부분을 이야기하거나 잘못된 부분에 대해 조심해서 얘기하면서 시간을 낭비해야 하죠? 그냥 바뀌어야 하는 부분을 직접 집어 말해줘서 빠르게 고치는 게 뭐가 잘못인가요? 음, 사람들은 감정이 있고 그 감정은 생산성에 영향을 준다는 점이 문제입니다.

상대방의 강점을 인지하지 않고 상대의 잘못을 냉담하게 지적한다면 상대방은 위협으로 느낄 수도 있으며 생산성에 악영향을 줄 수 있습니다. 데이비드 록은 다음처럼 설명합니다.

위협적인 응답은 분석적 사고, 창의적인 통찰, 문제 해결력을 훼손합니다.

즉, 가장 효과적이라고 생각하는 방식이 고통스러운 감정의 원인이 되고 실제로 팀의 발목을 붙잡는 일이 됩니다.

코드 리뷰에도 동일합니다. 만약 누군가의 오류를 억양 없이 지적하는 일은 역효과를 낳아 상대방의 열정을 죽이고 성장해야겠다는 동기를 짓누르는 일이 될 수 있습니다.

평화적인 대안

단순히 충분하지 않다는 이유만 갖고 거만한 녀석이 되는 일을 멈추기 바랍니다. 대신 팀에 긍정적으로 협력하고 진정성을 갖고 지원하기 바랍니다. 긍정적인 감정으로 팀의 다른 사람들에게 어떻게 최고의 영감을 줄 수 있을지 고민하고, 팀원이 고통받지 않도록 하는 일까지 한 단계 앞서 나갈 필요가 있습니다.

기계에 대고 말하는 것이 아니라 사람과 대화한다는 사실을 잊지 마세요. 카렌 암스트롱은 다음처럼 경고했습니다.

"만약 자비로움과 공감이 제련되어 있지 않다면, 이성은 인간을 도덕적 공허로 이끌 수 있습니다."

피드백으로 돌아와서, 여기에 사용할 수 있는 하나의 기법은 진정성 있는 질문에 의지하는 방법입니다. 위에서 이야기한 애완동물 앱의 예제 피드백은 "하지 않나요?"로 끝나고 있습니다. 문장은 질문의 형식을 빌리고 있지만 평가하는 문구에 가깝습니다. "이 화면의 제목 선택에 관해 설명해주실 수 있을까요?" 정도가 더 나은 선택지가 되겠습니다.

또한 비판적인 피드백도 긍정적인 피드백과 함께 제련할 수 있습니다. 어떤 사람들은 "샌드위치" 기법으로 피드백을 주는 것을 좋아하기도 합니다. 먼저 칭찬할 점을 언급하고, 건설적인 비판을 제공한 후 또 다른 칭찬을 합니다. 또 어떤 사람들은 (저 자신을 포함해서) 투명하게 보고 요점만 말하길 원하는 경우도 있습니다.

저도 이 조언에 저항하는 데 참여할 수 있습니다. 제 경력 대부분 동안 이 조언에 저항했기 때문입니다. 어린 에이프릴은 "뭐, 사람들이 진실을 대하는 방법에 대해 배워야 할 뿐이라고" 같은 말을 했었습니다. 아마 제 그런 과거에서 저항하려는 이유가 있을 겁니다. 하지만 사람들과 강한 관계를 만들어가는 과정에서 서로 굳은 신뢰 관계(rapport)가 없는 사람과 직설적인 의사소통을 한다면 부적절할 수 있습니다. 저는 또한 진정으로 자아 없는 프로그래밍과 제품 개발도 염두에 두는데 이런 경우에는 피드백은 직접 줄 수 있을 겁니다. 이런 경우에는 (이론적으로) 자아가 없기 때문이죠.

그리고 사람 일은 모르는 법입니다. 직접적인 피드백을 줘도 잘 받는 누군가가 있다 하더라도 그 피드백을 받는 그 날에 그 사람의 개가 죽었다면 평소처럼 받지 못하고 무너질지도 모릅니다. 그래서 어느 때라도 소통하는 사람들의 감정에 당신의 말이 어떤 영향을 주는지 고려하지 않아도 되는 경우는 차라리 없다고 생각해야 맞습니다.

비관적 말투

마지막 독성 말투는 비관적 말투입니다. 먼저 저는 회의적인 입장도 어느 정도 팀에 존재하는 것이 건강하다고 생각하는 편이라서 그런 경우는 문제가 아닙니다.

문제는 거의 모든 창의적인 새 아이디어, 도구, 접근 방식에 대해서 비관적으로 대하는 경우인데 "다 본 적이 있다"고 말해서 씁쓸하게 만드는 시니어 엔지니어 같은 경우가 일반적입니다. 이런 건 정말 다 최악입니다.

"그거 동작 안 할 거야."

"그거 확장 안될 거야."

"그 새 도구는 그냥 예전 것만큼 별로야."

어떤 이유에서인지 어떤 엔지니어는 종종 전 USC 총장인 스티븐 샘플이 말한 "습관적 반대론자"에 속하기도 합니다. 그는 다음처럼 설명합니다.

"새로운 아이디어가 어떻게 동작 가능할지 상상하는 일보다 이들은 본능적으로 왜 안 되는가에 대한 온갖 이유를 생각합니다. 이 사람들은 이런 태도가, 나쁘거나 멍청한 아이디어에 쓰는 모든 사람의 시간을 아낀다고 굳게 믿고 있습니다. 하지만 이 사람들이 실제로 하는 일은 자유로운 생각에서 수확할 수 있는 창의력을 침식하는 일입니다."

엔지니어가 비관주의의 매력을 느끼는 이유가 두 번째 문장에서 정확하게 묘사되고 있습니다. 시간을 절약하는 것 말입니다. 당신의 비관주의적 예측이 옳다면 그들의 아이디어를 거부함으로 모든 사람들의 시간을 잘 절약하고 있을 것입니다.

하지만 이 일이 너무 자주 일어나면 엔지니어는 그 아이디어가 정말 실패할지 잘 모르는 상황에서도 작은 확신이라도 있으면 아이디어를 거절하게 됩니다. 모든 사실을 검증하기도 전에 판단을 내립니다. 아마 과거에 비슷한 접근 방식이 실패한 것을 봤을 수도 있습니다. 또는 수많은 불필요한 프로젝트 관리 도구를 지난 기간 동안 봐서 어떤 새로운 도구든 제대로 동작할 거란 상상을 하지 못할 수도 있습니다. 아니면 매번 위키를 도입하려고 시도했지만, 팀이 계속 관리하지 못했기 때문에 이번에도 별다르지 않을 것으로 생각할 수도 있습니다.

이 모든 경험은 엔지니어가 팀에게 제공할 수 있는 유용한 정보이긴 하지만, 그렇다고 해서 제안이 실패할 것이라는 결론에 즉각적으로 닿지는 않습니다. 과거의 데이터를 알려주면서 거기에 한숨과 부정적인 말투를 더할 필요는 분명 없습니다.

아마 과거 프로젝트에서 레일즈를 사용하면서 한 번의 나쁜 경험이 있을 수 있습니다. 그 한 번의 경험으로 나쁜 도구라고 말할 순 없습니다. 아마도 당시 상황에 맞지 않았거나 팀이 전체적으로 이해하고 있지 못했을 수도 있습니다.

오랜 기간에 걸쳐 많은 프로젝트의 실패를 봐 온 엔지니어라면 실패를 예측할 수 있습니다. 이런 엔지니어는 그들이 말하는 모든 것에 일반적인 부정론을 투영하기도 합니다. 팀을 막다른 골목에 들어가서 시간을 낭비하는 일을 구하기 위해 매번 반대하는 말을 하게 되면, 아마 팀의 의욕을 꺾고 달콤한 실험을 방해했을 가능성이 10배는 될 겁니다.

또한, 위험할 수도 있는 부분은 새로운 엔지니어가 이런 독한 기운을 뿜는 엔지니어를 경외하기도 한다는 점입니다. 이런 이유의 배경을 짐작해보면 모든 것에 불평하고 반대하는 것 보면 분명 모든 걸 다 알기 때문이라 생각하는 것으로 보입니다. 이 모든 부분이 악순환입니다.

Sidenote: 흥미롭게도 이런 엔지니어는 의외로 낙천적인 경향이 딱 한 주제에서 나타나는데 바로 작업을 완료하는데 걸리는 시간입니다. 이런 낙관주의적 경향의 주된 결과는 한심할 정도로 현실적이지 못한 시간 예측입니다.

평화적인 대안

엔지니어가 실패에 대한 가능성을 볼 수 있다면 유용할 겁니다. 이런 관점이 버그와 다운타임, 보안 문제를 방지하기 때문입니다. 하지만 성공의 가능성과 잠재적인 부정적 결과를 발견하는 능력에 균형을 찾는 일은 진정 주의해야 합니다.

그 이유는 숀 머피의 The Optimistic Workplace 설명에서 확인할 수 있습니다.

"두뇌는 기쁨과 같은 긍정적 감정에 열리게 될 때, 전체적인 연결성을 명확히 볼 수 있고 문제를 해결할 수 있는 선택지를 찾을 수 있습니다."

그리고 통계도 인용합니다.

"긍정적인 업무 환경에 있는 사람들은 부정적인 분위기에서 일하는 사람보다 10~30% 더 나은 결과를 냈습니다."

아이디어를 내리깎고 새 프로젝트의 실패를 예측하기 전에 스스로 물어보세요. 정말로 이 아이디어가 실패한다고 확신하나요? 볼테르가 확신에 대해 한 말을 기억하기 바랍니다.

"사기꾼만 확신에 차 있습니다. ... 의심이란 그다지 바람직한 상태가 아닙니다. 하지만 확신이란 얼토당토않은 상태인 것입니다."

확신하지 않으면서 왜 확신한 것처럼 말할까요? "그거 동작 안 할 거야"는 정말 당신의 응답으로 필요한 말일까요? 이런 대답 대신에 공손하게 어떤 걱정이 있는지, 과거에 팀에서 어떤 경험을 했는지 공유하는 것은 어떨까요?

만약 정말 그 비운의 아이디어가 실패한다고 확신 한다면 먼저 볼테르의 말을 다시 읽어보고, 자신의 의심을 나머지 팀원에게 의욕을 꺾지 않는 방법으로 어떻게 소통할지 고려하기 바랍니다. "맞아요, 그리고...?"로 그 응답을 시작할 수 있습니다. 예를 들면, "맞아요, 그 아이디어가 뛰어난 이유를 알겠어요. 그리고 이 아이디어는 이런 다른 맥락에서 더 좋은 이유가 있어요." 또는, "맞아요, 흥미로운 아이디어네요. 그리고 아마도 몇 달 후에 Y를 달성하고 나면 그 아이디어가 더 실현할 수 있겠네요." 식으로 답할 수 있을 겁니다.

여기에 자신에게 자비로움을 베풀어 도움을 구할 수 있는 시간이 또 있습니다. 다른 무언가로 인해 괴로워하고 있고 그 고통이 제안된 아이디어에 대해 반대하는 진짜 원인인가요? 그냥 피곤한가요? 먼저 본인에게 있는 개인적인 필요를 돌봐야 합니다. 그리고 그 아이디어를 여전히 반대하는지 살펴보기 바랍니다.

그리고 회사의 사명과 자기 자신의 사명을 다시 상기하는 일도 도움 됩니다. 왜 이 프로젝트에 지금 일하고 있죠? 무엇이 희망을 주나요? 그 희망을 새로운 아이디어에 대한 열린 마음으로 이어갈 수 있나요?

항상 모든 일이 완벽하게 잘 될 것이라고 가장할 필요는 없습니다. 하지만 새 아이디어에 대한 열린 태도는 진정으로 혁신적인 작업을 하기 위한 전제조건입니다. 또한 당신을 더 즐겁고 타인을 지지해줄 수 있는 동료로 만들 것입니다.

미래

"컴퓨터 프로그래밍은 인간 활동입니다. ... 하지만 아직도 ... 많은 사람들(많은 프로그래머들)은 프로그래밍을 인간 활동으로 전혀 고려하고 있지 않습니다."

제럴드 와인버그는 1971년에 이 글을 썼지만, 여전히 이런 인식이 존재합니다. 제 생각에는 엔지니어링에서의 커뮤니케이션이 주목받을 가치가 있습니다.

우리가 아름다운 소프트웨어 걸작을 만드는 일을 사랑하는, 뛰어난 엔지니어라면 그와 동시에 친절하고 자비로운 사람들로 서로를 돕고 말과 글로 의사소통을 할 때는 서로의 단어로 영감을 불어넣는 사람이 될 수 있지 않을까요?

저는 기술 업계의 더 밝은 미래를 그립니다. 모든 종류의 독성 말투를 버리고 대신에 긍정적이고, 겸손하며, 희망차고, 명확하며, 초대하는 것과 같은 말투를 받아들인 미래를 말입니다. 왜 초대하는 것과 같아야 하는가 하면 알다시피 우리가 만들고자 하는 것을 모두 만들려면 우리에게 더 많은 엔지니어와 기여자를 필요로 하기 때문입니다.

프란 앨런의 Coders at Work 인터뷰에서 말하는 엔지니어링처럼 말입니다.

"엔지니어링은 사회 전체를 위한, 변혁적인 분야입니다. 또한 우리가 하는 일에서 다양한 사람들의 참여 없이는, 우리 사회의 모든 측면에서 매력적이거나 유용하지 않은 결과를 얻게 됩니다."

그러므로 진보적인 관점에서, 불필요하고 자아 중심적인 부정성 때문에 다른 누군가가 겁먹지 않도록 조심합시다.


  • 2018-12-12: 직진적직선적으로 변경했습니다. 버블쓰님 피드백 감사드립니다.
  • 2018-12-12: 현명이라고 생각했습니다가장 현명한 사람이라는 생각이 들었습니다으로 변경했습니다. minieetea님 피드백 감사드립니다.
  • 2018-12-13: 잘못 걸린 링크를 수정했습니다. 정겨울님 피드백 감사드립니다.
  • 2018-12-13: 제럴드 와인버그의 인용을 수정했습니다. chiyodad님, lazygyu님, jinhoyim님 피드백 감사드립니다.
  • 2019-07-16: 지정지적으로 고쳤습니다. 아델라님 피드백 감사드립니다.

지난달 Hacktoberfest 2018이 열린다고 트위터에서 알게 되었다. 이번이 5회차라고 하니 여러 해 있던 행사인데 어떻게 한번도 몰랐나 싶었다. 그동안 몇 달 코드를 작성할 일이 전혀 없어서 나는 정말 개발자인가 😢 싶을 정도였는데 오랜만에 코드를 작성해서 즐거웠다.

PR을 보내며 느낀 점, 더 배우고 싶은 부분을 적어야겠다 생각했었다. 그런데 월 중순부터 여행을 다녀온 이후로 정신이 없어 까맣게 잊고 있었는데 오늘 참여 기념품이 덜컥 도착했다. 더 잊기 전에 짧게라도 작성하려고 한다.

I just got a package from #Hacktoberfest! 😊😊😊

참여 방법은 간단했다. 10월에 GitHub을 통해서 5건의 Pull Request를 보낸다. 물론 몇 가지 품질 표준으로 제시한 내용(빈칸 고치기 같은 것을 스크립트로 돌리지 않는다, 다른 사람의 코드로 PR을 보내지 않는다거나 하는 등)에 맞춰야 했다.

보낸 Pull Requests

원래 hacktoberfest에서 PR을 확인할 수 있었는데 끝나서 그런지 더는 해당 페이지를 제공하지 않는 것 같다. 분명 접근할 수 있을 텐데 주소를 찾지 못하겠다. 대신에 Hacktoberfest Checker에서 보낸 PR을 확인할 수 있다.

geokrety/geokrety-website

"label:hacktoberfest"로 검색하던 중에 찾은 리포지터리다. php로 만든 geocaching 웹사이트인데 php도 할 줄 알고 geocaching도 좋아하는 주제라서 더 살펴보게 되었다. 코드가 엄청 깔끔하진 않지만, 이슈 내용으로는 내가 쉽게 도울 수 있는 부분이라서 코드를 보냈다.

코드 내에서 영어가 아닌 폴란드어를 자주 사용하고 있다는 점이 흥미로웠다. 예를 들면 search.php가 아니라 szukaj.php라는 파일명을 쓰거나 변수명이 name 대신 nazwa를 사용한다. 테이블 이름도 테이블 필드명도 부분마다 그랬다. (그래도 영어와는 다르긴 하지만) 알파벳을 사용하는 국가에서는 큰 어려움 없이 프로그래밍에 사용할 수 있다는 장점이 있었다. 여기서 구글 번역기의 힘이 크게 도움 되었다.

만약 반대 관점에서 한글을 로마자로 작성한 경우라면 어떨까? 가끔 용어 적절성 등의 이유로 한국어를 영어로 옮기지 못하는 경우를 종종 마주하기도 했다. 그렇다고 한글을 직접 사용하지 못하고 로마자 표기법대로 변환해서 사용하게 된다. 한국어를 전혀 모르는 사람이 이런 코드에 기여한다고 상상해보면 전혀 알 수 없는 단어를 번역기에 넣기 전에 일단 로마자-한글 변환을 해야 한다는 점부터 알아야 하는데 그렇게 쉽지는 않다. 이런 생각을 하니 폴란드어가 조금은 부러워졌다.

만약 이런 한글-로마자 변환을 써야 하는 경우라면 CONTRIBUTING.md 같은 곳에 어떻게 변환하고 찾아야 하는지 정리해주면 좋겠다. 별도로 glossary 페이지를 작성하는 것도 공수가 들긴 하겠지만 도움이 될 것 같다.

docker-compose가 들어있긴 했지만, 생각처럼 한 번에 돌지는 않았고 내 환경에 맞춰서 수정해야 했다. 그래도 어디를 보고 수정해야 하는지도 INSTALL.md에 문서화되어 있어서 다행이었다.

프론트 컨트롤러 패턴을 사용하고 있지 않은 php 웹사이트라서 보안도 그렇고 고쳐야 할 부분이 많이 보였다. 일단은 이슈 중심으로 PR을 보냈고 시간대 차이 덕분에 빠르게 피드백을 받을 수 있었다.

totuworld/time-recorder-viewer

어디 기여할 곳 없는가 기웃거리고 있었는데 totu님이 리포지터리 주소를 던져주셨다.

작업시간 기록 도구인 time recorder의 웹 뷰어인데 typescript로 작성하셔서 보는 재미가 있었다. typescript를 프로젝트에서 사용해본 적이 없었는데 express.js에 asp.net mvc처럼 쓰고 있어서 신기했다. react도 그렇고 너무 화려하다! 얼른 부트스트랩 프로젝트로 뜯어서 공개해주셨으면 좋겠다. 😇

어디에 기여해야 하는가 생각하며 코드를 보다가 쓰지 않는 파일이 있길래 파일을 제거하는 PR을 보냈다. 품질 표준 기준에 미치지 않는 것 같아 좀 찔린다. 하하하...

gatsbyjs/gatsby

Gatsby는 React 기반의 사이트 생성기다. 얼마 전에 이 블로그도 Gatsby로 옮겨와서 익숙한 이름이기도 했고, 커뮤니티 분위기라든지 너무 좋은 프로젝트라 뭐라도 기여하고 싶었었다. 문서화도 정말 잘 되어 있어서 환경도 쉽게 구성해 시작할 수 있었다.

단일 리포지터리를 쓰는 프로젝트는 처음 봐서 이걸 여기에 이렇게 보내는 게 맞나 싶었다. 이슈는 온갖 다양한 주제로 가득했고 PR도 어떻게 담당하는 사람을 찾아가는지 대단하다 싶었다. 그래도 모든 창구가 단일화되어 운영되는 것이 오히려 전체적인 흐름을 파악하기도 쉽고 다른 의존적 코드를 참조하거나 찾는 일도 쉬웠다. 처음 리포지터리를 받아서 테스트를 돌리는데 리포지터리 내에 있는 모든 패키지가 교통정리 되면서 모든 테스트가 한꺼번에 진행되는 모습이 인상적이었다. 구조적인 장점 때문인지 몰라도 그동안 봤던 어느 프로젝트보다 많은 CI가 달려 있었다. 코드를 작성하면서 단일 리포지터리의 좋은 점에 언급된 장점 대부분을 몸소 느낄 수 있었다.

Gatsby의 단일 리포지터리는 lerna를 사용해서 운영되고 있다. lerna를 처음 봤을 때는 왜 이런 수고를 하지 싶었는데 실제로 운영되는 모습을 보고 나니 역시 나는 하나도 모르는구나, 탄식했다. 조만간 사용해보고 싶다.

이 블로그에 아직 한국어 포스트만 많은데 영어 포스트도 할 생각으로 영어 포스트와 한국어 포스트의 RSS 피드를 따로 생성하고 있었다. RSS 리더기는 대부분 웹사이트를 등록하면 rss 주소를 찾아내는 auto discovery 기능을 제공한다. 이 과정에서 <link>title을 읽어 해당 피드의 이름으로 사용하는데 gatsby의 gatsby-plugin-feed는 이 필드를 설정하는 부분이 없었다. 그래서 이 부분을 추가해서 코드를 보냈다.

Gatsby에서 gatsby-config.js플러그인을 설정하고 용도에 따라 플러그인 내에 gatsby-*.js를 읽어가는 부분이 흥미로워서 관심 있게 살펴봤다. 플러그인도 그렇고 확장성을 염두에 두고 메시지 기반처럼 작성된 부분이 많았다.

그리고 jest를 테스트에 사용하고 있는데 스냅샷 테스팅이 두루두루 적용되어 있었다. 처음 __snapshots__ 디렉토리를 보고 의아했는데 찾아보고서는 반하지 않을 수 없었다.

참고로 Gatsby는 기여를 하면 Gatsby Store에서 사용할 수 있는 $10 쿠폰을 준다! Hacktoberfest가 시작되기 전에 문서에 있던 잘못된 링크를 고친 적이 있었는데 이때 받은 쿠폰으로 재빠르게 스티커랑 티셔츠를 샀다.

I regret everything

내 웹사이트 니꺼보다 빠름 💜

짧게는 행동으로, 길게는 영향으로

오픈소스 소프트웨어 커뮤니티는 우리 이전의 사람들 덕분에 지금 자리에 있을 수 있습니다. 당신의 참여는 10월(Hacktoberfest) 이후에도 많은 사람과 기술에 지속적인 영향을 줍니다. 이 과정은 경쟁이 아닌 여행입니다.

Hacktoberfest의 가치

몇 안 되는 사소한 코드긴 했지만 hacktoberfest를 기회로 오픈소스에 참여해볼 수 있어서 좋았다. 항상 사용자 입장에 있던 탓인지 언제든 이슈를 열고 PR을 보낼 수 있는 시대가 되었어도 참여는 늘 멀게만 느껴졌다. 겨우 몇 개의 PR을 작성했을 뿐이지만 앞으로는 무슨 코드든 마음 편하게 작성해서 보낼 수 있을 것 같다.

그리고 오픈소스를 프로젝트를 운영하는 방식, 라이브러리와 패키지, 마음 편히 코드를 보낼 수 있도록 돕는 도구가 얼마나 중요한지 또 상기했다. 좋은 방법과 도구는 부지런히 익숙해지도록 써보고 언제든 꺼낼 수 있도록 깊이 있게 배워야겠다.

마지막으로 작은 코드나 문서의 오타를 수정하는 정도지만 오픈소스 세상은 충분히 커서 내 조그마한 기여도 정말 많이 필요로 하는 것 같다. "경쟁이 아닌 여행"이란 부분을 상기한다. 앞으로도 얼마나 많이 기여할 수 있을지 모르겠지만 틈틈이 참여할 수 있으면 좋겠다.

지난 달에 있던 Hacktoberfest에 참여하면서 Gatsby에 기여를 하게 되었습니다. 단일 리포지터리로 된 프로젝트는 처음 경험해서 코드가 반영되고 갱신되는 과정이 흥미로웠습니다. 단일 리포지터리를 사용하면 다중 리포지터리에 비해 어떤 장점이 있는지 여러 글을 읽어보게 되었는데 그 중 하나를 번역하게 되었습니다.

이 글은 Dan LuuAdvantages of monorepos 번역입니다.


단일 리포지터리(monorepo)의 좋은 점

다음 같은 대화를 자주 합니다.

누군가: 페이스북/구글에서 거대한 단일 리포지터리를 사용한다는 얘기 들었어? 뭐임 대체!

: 그래, 엄청 편하겠다. 그럴 것 같지 않아?

누군가: 엥 내가 들어본 얘기 중에 가장 황당한 얘기다. 페이스북이나 구글은 단일 리포지터리에 모든 코드를 넣는 게 얼마나 끔찍한 아이디어인지 모르는 건가?

: 내 생각에는 페이스북, 구글에서 일하는 엔지니어도 작은 크기 리포지터리가 익숙할 거야. (Junio Hamano도 구글에서 일하지 않나?) 그리고 여전히 단일 거대 리포지터리를 선호하는데 거기에는 이런 [이유]가 있어.

누군가: 흠, 그래, 꽤 괜찮은 것 같네. 내 생각엔 여전히 이상하긴 하지만 왜 이런 리포지터리를 원하는지도 이해할 수 있을 것 같다.

"[이유]"는 생각보다 꽤 깁니다. 그러니 같은 대화를 계속 반복하는 것보다 글로 작성해서 이유를 설명하려고 합니다.

단순화된 편성

다중 리포지터리를 사용한다면 일반적으로 프로젝트 당 리포지터리가 있거나 연관된 프로젝트를 기준으로 리포지터리가 있을 겁니다. 하지만 "프로젝트"가 무엇인가에 대한 정의는 어떤 팀 또는 회사에 있는가에 따라 달라질 겁니다. 이런 이유로 리포지터리를 합치거나 분리하게 되는데 이런 작업은 순수하게 부가적인 비용이 됩니다. 어떤 경우에 프로젝트를 분리하게 되는지 예를 들면 프로젝트가 너무 큰 경우, 또는 버전 관리 도구의 이력이 너무 많아 최적화가 필요한 경우에 프로젝트를 분리하게 됩니다.

단일 리포지터리를 사용하면 버전 관리 도구가 특정 방식으로 구조를 구성하도록 하는 것이 아니라 논리적으로 가장 일관적인 방법으로 프로젝트를 편성하고 묶을 수 있습니다. 또한 단일 리포지터리를 사용하면 의존성을 관리하는 부하를 줄일 수 있습니다.

편성 단순화의 부수 효과는 프로젝트 간 탐색을 더 쉽게 할 수 있다는 점입니다. 단일 리포지터리를 사용하면 기본적으로 파일 시스템에서 파일을 탐색한다고 말하는 것과 동일하게 모든 프로젝트를 탐색할 수 있습니다. 다중 리포지터리로 구성하면 대부분 두 계층으로 분리된 탐색이 필요합니다. 먼저 프로젝트 안을 탐색할 때는 파일 시스템에서 말하는 탐색이 필요하고, 메타 계층으로서 프로젝트 사이를 찾는 탐색도 필요합니다.

단일 리포지터리를 사용하면 나타나는 이 부수 효과에는 또 부수 효과가 있는데 아주 쉽게 개발 환경을 꾸리고 빌드, 테스트까지 구동할 수 있다는 점입니다. 다른 프로젝트를 탐색하기 위해서 cd를 사용할 수 있고 cd; make로도 당연히 가능하게 됩니다. 이런 구조가 동작하지 않는 것은 당연히 이상하게 보일 정도로 제대로 동작합니다. 동작하기 위해 필요한 어떤 툴링 작업이든 문제없이 완료됩니다.1 물론 기술적으로 다중 리포지터리를 사용하더라도 동일한 작업을 할 수 있지만 자연스럽지는 않습니다. 즉, 생각처럼 동작하지 않을 때가 종종 있다는 의미입니다.

단순화된 의존성

아마 이 사실은 말할 필요도 없겠지만 다중 리포지터리를 사용하면 서로 어떤 버전에 의존하고 있는지 정의하는 방법이 필요할 것입니다. 듣기에는 복잡하지 않은 문제라고 느낄지 몰라도 실무에서는 대부분의 해결책이 성가신 데다 많은 부하가 따릅니다.

단일 리포지터리를 사용하면 모든 프로젝트를 위한 단일 버전을 쉽게 만들 수 있습니다. 여러 프로젝트에 걸쳐서 원자적인 커밋(atomic cross-project commit)이 가능하기 때문에 리포지터리는 언제나 일관적인 상태를 유지할 수 있습니다. 즉, #X 커밋에 모든 프로젝트 빌드가 동작해야 합니다. 빌드 시스템에서는 여전히 의존성을 지정할 필요가 있지만 Makefile이나 bazel BUILD 파일을 사용할 때는 다른 것과 같이 버전 관리 내에서 확인할 수 있습니다. 그리고 단 하나의 버전 번호를 갖게 되는 것으로 Makefile이나 BUILD 파일, 또는 어떤 빌드 방법을 사용하더라도 특정 버전 번호를 지정할 필요가 없게 됩니다.

툴링(Tooling)

탐색과 의존성을 단순하게 하면 도구를 만드는 일도 훨씬 쉬워집니다. 도구를 만들며 각 리포지터리의 관계를 이해하도록 하는 방법 대신에 모든 파일이 리포지터리 내에 들어 있기 때문에 이런 도구는 (리포지터리 내에 의존성을 정의한 몇 파일을 포함해서) 그저 파일을 읽는 일만 하면 됩니다.

이 부분은 사소한 것처럼 느껴질 수 있겠지만 Christopher Van Arsdale의 예에서 빌드가 얼마나 단순하게 가능한지 볼 수 있습니다.

구글 내부의 빌드 시스템은 대형 모듈러 블록 코드를 사용해서 빌드를 환상적으로 쉽게 수행합니다. 크롤러가 필요해요? 여기에 몇 줄 추가합니다. RSS 파서가 필요해요? 몇 줄을 더 추가합니다. 대형 분산, 장애 허용 데이터 저장소? 물론 몇 줄을 더 추가합니다. 이렇게 만들어진 코드 블록과 서비스는 여러 프로젝트에서 공유되며 쉽게 통합할 수 있습니다. … 이처럼 레고(LEGO)처럼 조립 가능한 개발 프로세스는 오픈소스 세계에서 자주 일어나지 않습니다. … 이런 다양한 상태의 결과로 (추측하건대) 오픈소스에 복잡한 장벽이 생겨나고 지난 몇 년간 큰 변화가 없었습니다. 이런 이유에서 구글과 같이 쉽게 관리하는 경우와 그러지 못한 여러 오픈소스 프로젝트의 차이를 만들게 되었습니다.

Arsdale이 편하다고 말한 이 시스템은 오픈소스가 되기 전에 전 구글 엔지니어가 페이스북, 트위터에서도 동일한 혜택을 누리기 위해서 bazel의 자체 버전을 작성했습니다.

이론적으로는 단순히 단일 리포지터리 없이도 모든 의존성을 해결하고 빌드를 수행하는 빌드 시스템을 만들어 낼 수 있습니다. 하지만 더 큰 노력이 필요한 데다 이런 노력에도 불구하고 문제없이 돌아가는 시스템을 본 적이 없습니다. Maven과 sbt는 그런 점에서 꽤 잘 만들어지긴 했지만, 버전 의존성 문제를 추적하고 고치는데 많은 시간을 들이는 것 또한 일반적으로 많이 겪게 됩니다. rbenv나 virtualenv와 같은 시스템은 문제를 우회하려는 노력이지만 개발 환경을 급증하게 하는 결과를 만들었습니다. 단일 리포지터리에서 HEAD는 항상 일관적이고 유효한 버전을 가리키고 있으며 다중 리포지터리의 버전을 추적하는 노력을 완전히 제거하게 됩니다2.

단일 리포지터리를 운영하면서 빌드 시스템에만 이득이 있는 것이 아닙니다. 더 예를 들자면 프로젝트 경계를 넘어서 정적 분석을 수행하는 일에도 추가적인 노력이 필요하지 않습니다. 프로젝트 간의 통합 테스트나 코드 검색과 같이 많은 부분에서도 훨씬 단순해집니다.

프로젝트 간 변경

많은 리포지터리를 갖고 있다면 프로젝트 간 변경은 고통스럽습니다. 일반적으로 각각의 리포지터리를 걸쳐 정합성을 맞추기 위해 어마어마한 양의 수작업이 따르거나 또는 핵과 같은 스크립트를 작성해서 돌려야 합니다. 스크립트가 동작하더라도 프로젝트 간 버전 의존성을 올바르게 고치기 위한 부하도 발생합니다. 10개의 내부 프로젝트에 걸쳐 사용하고 있는 API를 리팩토링한다면 아마 하루종일 시간 쓰기 좋은 분량일 것입니다. 수천 개의 내부 프로젝트에서 사용하고 있는 API를 리팩토링하는 경우라면 꿈도 희망도 없습니다.

단일 리포지터리라면 API와 모든 호출자의 리팩토링은 커밋 하나로 해결할 수 있습니다. 항상 이런 사소한 작업이 있는 것은 아니지만 수많은 작은 리포지터리를 수정하는 방법보다 훨씬 쉽습니다.

대부분은 CVS, RCS, ClearCase와 같은 버전 관리 도구를 사용하는 일이 불합리하다고 생각하고 있습니다. 이런 버전 관리 도구는 여러 파일에 걸친 원자적 커밋을 할 수 없습니다. 그래서 커밋의 타임 스탬프와 커밋 메시지, 또는 "진짜" 원자적으로 커밋된 파일이 어떤 것인지 확인할 수 있는 메타 정보를 확인해야 합니다. SVN, hg, git 등과 같은 도구는 여러 파일 변경도 하나의 커밋에 넣을 수 있기 때문에 이 문제를 해결할 수 있습니다. 단일 리포지터리는 동일한 문제를 여러 프로젝트에 걸쳐 해결할 수 있습니다.

이 접근 방법의 유용성은 단순히 대규모의 API 리펙토링에만 국한되지 않습니다. David Turner는 트위터에서 여러 리포지터리를 단일 리포지터리로 옮기는 작업을 했습니다. 이 작업은 작은 일이지만 여러 프로젝트에 걸친 변경과 이 변경을 배포하는 과정에서 어떤 부하가 발생하는지 이야기합니다.

[프로젝트 A]를 갱신해야 합니다. 하지만 이 작업을 위해서는 내 동료가 이 프로젝트의 의존성인 [프로젝트 B]에서 고친 코드가 필요합니다. 이 동료는 [프로젝트 C]에서 변경된 내용이 필요합니다. 만약 A를 고쳐서 배포하려 한다고 해도 C의 배포를 기다려야 하고 B 배포도 기다려야 하는 상황입니다. 하지만 모든 것이 하나의 리포지터리에 있다면 내 동료가 코드를 변경해서 커밋 하자마자 내 코드 변경도 즉시 가능합니다.

물론 git 버전에 의해 모든 것이 연결되어 있다면 다중 리포지터리에서도 가능할 겁니다. 하지만 내 동료는 두 개의 커밋이 필요할지도 모릅니다. 이런 작업에는 버전을 하나 집어서 "안정화" (더 움직이지 않도록) 하고 싶은 욕망이 항상 생깁니다. 하나의 프로젝트라면 문제가 없겠지만 상호의존적인 복잡한 프로젝트에서는 이조차도 그다지 쉽지 않은 방법입니다.

[다른 방향에서 본다면] 의존하는 부분 이 강제로 갱신돼야 하는 상황인데 이 또한 단일 리포지터리의 장점이라고 할 수 있습니다.

이 접근 방법은 단순히 프로젝트 간 변경을 쉽게 수행할 수 있는 것만 아니라 변경을 추적하는 일도 쉽게 만듭니다. git bisect을 여러 리포지터리에서 수행한다면 분명 다른 도구를 이용해서 메타 정보를 읽는 방법을 배워야 할 것이고 대부분 프로젝트에서는 그냥 간단하게 이런 작업을 하지 않아버리고 맙니다. 만약 이런 일을 한다고 해도 하나의 도구만 써도 충분한 작업을 여러 도구에 걸쳐서 하게 될 겁니다.

Mercurial과 git은 멋집니다! 실화입니다

CVS 또는 SVN에서 git 또는 hg로 변경한 경우에 가장 일반적으로 들을 수 있는 이야기는 엄청나게 생산성이 좋아졌다는 얘기입니다. 이 점은 사실입니다. 하지만 대부분 이런 응답의 관점은 git과 hg가 더 발달한 여러 측면(예를 들면 더 나은 코드 머지를 지원한다던가)이 그 이유였지 작은 리포지터리가 더 낫기 때문인 것은 아니었습니다.

사실 트위터는 git을, 페이스북은 Mercurial을 대형 단일 리포지터리를 지원하기 위해 고쳐서 사용하고 있습니다.

단점

물론 단일 리포지터리에도 단점이 있습니다. 여기서는 단점을 적지 않을 생각인데 이미 충분히 많은 논의가 있었기 때문입니다. 단일 리포지터리는 다중 리포지터리에 비해 엄격하게 우월한 지위를 갖는 것은 아닙니다. 그렇다고 엄격하게 나쁜 것도 아닙니다. 제 관점에서는 누구든 꼭 단일 리포지터리로 옮겨야 한다고 얘기하지 않습니다. 단지 단일 리포지터리를 쓰는 일이 완전히 부당하지 않다는 얘기를 하고 싶었습니다. 구글, 페이스북, 트위터, 디지털오션과 엣시(Etsy)에서 수백, 수천, 수만 개의 작은 리포지터리를 운영하는 대신 단일 리포지터리 사용을 선호하는 점에는 분명 좋은 이유가 있어서 그럴 겁니다.

다른 논의

광범위한 토론에 함께 한 Kamal Marhubi, David Turner와 Leah Hanson에게 감사 말씀 전합니다. 이 토론에서 적어도 절반 이상의 아이디어가 나왔습니다. 또한 이 글에서 오타와 실수를 찾아 준 Leah Hanson, Mindy Preston, Chris Ball, Daniel Espeset, Joe Wilder, Nicolas Grilly, Giovanni Gherdovich, Paul Hammant, Simon Thulbourn에게 감사드립니다.

Footnotes

  1. 이 부분은 제가 일했던 하드웨어 회사에서도 맞는 말입니다. 저는 이 회사에서 NFS에서 동작하는 RCS를 버저닝하는 단일 리포지터리를 만들어서 사용했습니다. 물론 사람들이 중앙 리포지터리에서 파일을 수정할 수 있게 할 수 없으니 억지로 이 작업을 가능하게 누군가가 여러 스크립트를 작성했습니다. 이런 시스템을 추천하진 않습니다만 엄청나게 단일 리포지터리처럼 해킹해서 쓰더라도 단일 리포지터리의 장점을 누릴 수 있습니다.

  2. 적어도 최신 의존성을 관리하는 메커니즘이 있어야 할 것입니다. 물론 이런 작업은 구글에서 문제가 없는데 구글은 코드를 작성해도 많은 수의 직원이 그 코드에 의존하기 때문에 모든 외부 의존성을 단일 리포지터리에 넣어도 전체 직원 규모에서 보면 오히려 비용이 절감되기 때문입니다. 제가 보기에 작은 회사에서 이런 접근 방법에 이득을 얻기에는 너무 비용이 많이 든다고 생각합니다.

이 글은 Robin Wieruch8 things to learn in React before using Redux 번역입니다.


React에서 Redux 전에 배워야 할 8가지

상태 관리(State management)는 어렵습니다. React같은 뷰 라이브러리는 지역 컴포넌트 상태를 관리하는 일이 가능합니다. 하지만 이 상태는 특정 시점에서 확장해야 하는 일이 생깁니다. 리액트는 단순히 뷰 계층 라이브러리 입니다. 언젠가는 Redux와 같이 더 수준 높은 상태 관리 솔루션으로 넘어가는 결정을 하게 될 겁니다. 하지만 이 글에서는 Redux 열차에 올라타기 전에 React에서 알아야 하는 부분에 대해서 지적하고 싶습니다.

사람들은 간혹 React와 Redux를 함께 배웁니다. 하지만 거기에 문제점이 있습니다.

  • 지역 상태 (this.state)만 사용하는 경우에 왜 상태 관리에 확장 문제가 발생하는지 겪어보지 못합니다
    • 그래서 왜 Redux 같은 상태 관리 라이브러리가 필요한지 이해하지 못합니다
    • 그래서 너무 많은 보일러플레이트에 대해 불평합니다
  • React에서 지역 상태를 관리하는 방법을 배우지 못합니다
    • 그래서 모든 상태를 Redux에서 제공하는 상태 컨테이너에 담아두고 관리하려고 합니다
    • 그래서 지역 상태 관리를 전혀 사용하지 않게 됩니다

이런 문제점으로 인해서 React를 먼저 배우고 나중에 필요하다고 느낄 때 Redux를 배우도록 조언합니다. 확장 문제는 대형 애플리케이션에서만 나타납니다. 가끔 Redux를 사용하고 있으면서도 상태 관리 라이브러리가 필요하지 않은 경우가 있습니다. 책 The Road to learn React에서는 Redux와 같은 외부 의존성 없이 있는 그대로의 React로 애플리케이션을 만드는 방법을 설명합니다.

하지만 당신은 지금 Redux 열차에 올라타기로 결정했습니다. 그래서 이 글에서는 Redux를 쓰기 전에 React에서 알아야 할 내용을 살펴봅니다.

  • React에서의 지역 상태는 자연스럽다
  • React 함수형 지역 상태
  • React의 상태와 프로퍼티
  • React 상태 옮기기
  • React의 고차 컴포넌트
  • React의 Context API
  • React의 상태 컴포넌트
  • 컨테이너와 프레젠터 패턴
  • MobX 아니면 Redux?

React에서의 지역 상태는 자연스럽다

이미 언급했지만 가장 중요한 조언은 React를 먼저 학습하라는 점입니다. 컴포넌트에 지역 상태 즉, this.setState()this.state를 사용해서 생명을 불어 넣는 일을 피할 수는 없습니다. 이 방식에 익숙해져야 합니다.

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
  }

  render() {
    return (
      <div>
        Counter: {this.state.counter}

        <button
          type="button"
          onClick={() => this.setState({ counter: this.state.counter + 1 })}>
          Click!
        </button>
      </div>
    );
  }
}

React 컴포넌트는 초기 상태를 생성자(constructor)에서 정의하고 있습니다. 그런 후에 this.setState() 메소드를 사용해서 갱신할 수 있습니다. 상태 객체의 갱신은 얕은 병합(shallow merge)으로 수행됩니다. 그러므로 지역 상태 객체를 부분적으로 갱신하고도 상태 객체의 다른 프로퍼티는 손대지 않고 그대로 유지할 수 있습니다. 상태가 갱신된 후에는 컴포넌트가 다시 렌더링을 수행합니다. 앞에서 예로 든 코드에서는 this.state.counter의 갱신된 값을 보여줄 것입니다. 이 예제에서는 React의 단방향 데이터 흐름을 사용해 하나의 닫힌 루프(loop)를 작성했습니다.

React 함수형 지역 상태

this.setState() 메소드는 지역 상태를 비동기적으로 갱신합니다. 그러므로 언제 상태가 갱신되는지에 대해 의존해서는 안됩니다. 상태 갱신은 결과적으로 나타납니다. 대부분의 경우에는 이런 방식이 별 문제 없습니다.

하지만 컴포넌트의 다음 상태를 위해 연산을 하는데 현재 지역 상태에 의존한다고 가정해봅시다. 앞서 작성했던 예제에서는 다음처럼 작성했습니다.

this.setState({ counter: this.state.counter + 1 });

지역 상태(this.state.counter)는 연산에서 바로 그 시점의 상태로 사용했습니다. 그러므로 this.setState()를 사용해서 상태를 갱신하긴 했지만 지역 상태는 비동기 실행이 수행되기 전에 신선하지 않은 상태값을 사용해 연산하게 됩니다. 이런 점은 처음 보고 나서는 바로 파악하기 어렵습니다. 천 마디 말 보다 다음 코드를 보는게 더 빠를 것 같습니다.

this.setState({ counter: this.state.counter + 1 }); // this.state: { counter: 0 }
this.setState({ counter: this.state.counter + 1 }); // this.state: { counter: 0 }
this.setState({ counter: this.state.counter + 1 }); // this.state: { counter: 0 }

// 예상한 상태: { counter: 3 }
// 실제 갱신된 상태: { counter: 1 }

이 코드에서 확인할 수 있는 것처럼 지역 상태를 갱신할 때 현재 상태에 의존해서는 안됩니다. 이런 접근 방식은 버그를 만듭니다. 그래서 이런 상황에서는 다음과 같은 방식으로 지역 상태를 갱신합니다.

this.setState()에는 객체 대신 함수도 사용할 수 있습니다. 함수는 비동기적으로 this.setState()가 실행될 때, 함수 시그니처에 지역 상태를 전달합니다. 그래서 이 함수는 콜백 함수로 정확한 시점에 올바른 상태를 갖고 실행되기 때문에 문제 없이 사용할 수 있게 됩니다.

this.setState(previousState => ({ counter: previousState.counter + 1 }));

이 방법으로 this.setState()를 여전히 이용하면서도 객체 대신 함수를 사용해서 이전 상태를 활용할 수 있습니다.

추가적으로 프로퍼티(props)에 의존적인 갱신이 필요한 경우에도 이 접근 방식을 따라야 합니다. 비동기적 실행이 수행되기 이전에 부모 컴포넌트에서 받은 프로퍼티가 변경되어서 값이 이전 정보가 되는 경우가 있기 때문입니다. 그래서 this.setState()의 두 번째 인자로 프로퍼티가 전달됩니다.

this.setState((prevState, props) => ...);

이제 올바른 상태와 프로퍼티를 사용해서 상태를 갱신할 수 있게 됩니다.

this.setState((prevState, props) => ({ counter: prevState.counter + props.addition }));

객체 대신에 함수를 사용하면서 얻을 수 있는 또 다른 장점은 바로 상태를 갱신하는 방법을 격리된 상태에서 테스트 해볼 수 있다는 점입니다. 단순히 this.setState(fn)을 사용하는 함수를 추출한 다음에 독립적으로 둔 다음에 테스트가 가능하도록 작성할 수 있습니다. 이 함수는 입력으로 간단히 출력을 확인할 수 있는 순수 함수여야 합니다.

React의 상태와 프로퍼티

상태는 컴포넌트 안에서 관리됩니다. 이 상태는 다른 컴포넌트에 프로퍼티로 내려줄 수 있습니다. 이 컴포넌트는 프로퍼티를 사용하거나 더 깊히 자식 컴포넌트로 전달할 수 있습니다. 덧붙여 자식 컴포넌트는 부모 컴포넌트로부터 콜백 함수를 전달 받을 수 있습니다. 이렇게 전달 받은 함수를 사용하면 부모 컴포넌트의 지역 상태를 변경하는 일도 가능합니다. 기본적으로 프로퍼티는 컴포넌트 트리를 타고 내려갑니다. 상태는 하나의 컴포넌트에서 관리합니다. 하위 컴포넌트에서는 프로퍼티로 전달한 함수를 사용해서 상태를 관리하는 컴포넌트까지 거슬러 올라와 상태를 변경할 수 있습니다. 갱신된 상태는 프로퍼티로 다시 하위 컴포넌트로 전달됩니다.

컴포넌트는 전체적인 상태를 관리할 수 있으며 자식 컴포넌트에게 프로퍼티를 전달할 수 있습니다. 프로퍼티에 함수를 전달하는 방법으로 자식 컴폰넌트가 부모 컴포넌트의 상태를 변경할 수 있게 합니다.

하지만 자식 컴포넌트는 전달된 함수의 출처가 어디인지, 프로퍼티로 받은 함수가 어떤 동작을 하는지 알지 못합니다. 이 함수는 부모 컴포넌트의 상태를 변경할 수도 있지만 다른 일을 할 가능성도 있습니다. 자식 컴포넌트는 단순히 실행하는 역할을 합니다. 프로퍼티도 동일합니다. 컴포넌트는 받은 프로퍼티가 프로퍼티인지, 상태인지, 또는 부모 컴포넌트에서 파생된 프로퍼티인지 알 방법이 없습니다. 자식 컴포넌트는 그저 사용할 뿐입니다.

프로퍼티와 상태의 개념을 이해하는 일은 중요합니다. 컴포넌트 트리에서 사용하는 모든 속성은 프로퍼티와 상태로 (그리고 프로퍼티와 상태에서 파생된 속성으로) 나눌 수 있습니다. 무엇이든 상호작용이 필요한 경우에는 상태에 보관되어야 합니다. 그 외 나머지는 모두 프로퍼티 형식으로 전달합니다.

수준 높은 상태 관리 라이브러리를 사용하기 전에 컴포넌트 트리를 따라 프로퍼티를 보내본 적이 있어야 합니다. 가장 끝에 있는 자식 컴포넌트에서 특정 값을 사용하려고 중간 컴포넌트에서는 전혀 쓰지 않는, 수많은 프로퍼티를 전달하는 코드를 작성하면서 "분명 이보다 더 나은 방법이 있을 거야" 생각해본 적이 었어야 합니다.

React 상태 옮기기

이미 지역 상태 계층(local state layer)을 옮겼나요? 이 방식은 일반 React에서 지역 상태 관리를 확장하는데 가장 중요한 전략입니다. 상태 계층은 올릴 수도, 내릴 수도 있습니다.

다른 컴포넌트에서의 접근을 줄이기 위해 지역 상태 계층을 하위로 내릴 수 있습니다. 컴포넌트 A가 자식 컴포넌트로 B와 C를 갖고 있다고 상상해봅시다. B와 C는 A의 자식 컴포넌트로 동등합니다. 컴포넌트 A는 유일하게 지역 상태를 관리하며 자식 컴포넌트에 프로퍼티를 전달합니다. 덧붙여 B와 C에서 A의 상태를 변경할 수 있는 함수도 전달합니다.

          +----------------+
          |                |
          |       A        |
          |                |
          |    Stateful    |
          |                |
          +--------+-------+
                   |
         +---------+-----------+
         |                     |
         |                     |
+--------+-------+    +--------+-------+
|                |    |                |
|                |    |                |
|       B        |    |        C       |
|                |    |                |
|                |    |                |
+----------------+    +----------------+

이제 컴포넌트 A의 지역 상태 절반은 컴포넌트 C에서 프로퍼티를 통해 쓰고 있으며 컴포넌트 B에서는 전혀 사용하고 있지 않습니다. 게다가 C는 A 컴포넌트에서 C에서만 사용하는 상태만 제어할 수 있는 함수를 프로퍼티로 전달했습니다. 여기서 볼 수 있는 것처럼 컴포넌트 A는 컴포넌트 C를 대신해서 상태를 관리하고 있습니다. 대부분의 경우에는 한 컴포넌트가 자식 컴포넌트의 모든 상태를 관리하는 일에 큰 문제가 없습니다. 하지만 컴포넌트 A와 컴포넌트 C 사이에 다른 컴포넌트가 추가된다고 생각해봅시다. 컴포넌트 A에서 컴포넌트 C에 전달해야 하는 프로퍼티를 컴포넌트 트리에 따라 전달합니다. 컴포넌트 A는 여전히 컴포넌트 C의 상태를 관리하고 있습니다.

          +----------------+
          |                |
          |       A        |
          |                |
          |                |
          |    Stateful    |
          +--------+-------+
                   |
         +---------+-----------+
         |                     |
         |                     |
+--------+-------+    +--------+-------+
|                |    |                |
|                |    |        +       |
|       B        |    |        |Props  |
|                |    |        v       |
|                |    |                |
+----------------+    +--------+-------+
                               |
                      +--------+-------+
                      |                |
                      |        +       |
                      |        |Props  |
                      |        v       |
                      |                |
                      +--------+-------+
                               |
                      +--------+-------+
                      |                |
                      |                |
                      |        C       |
                      |                |
                      |                |
                      +----------------+

이런 경우가 React의 상태를 아래로 내려야 하는 완벽한 경우입니다. 컴포넌트 A는 컴포넌트 C의 상태를 관리하고 있지만 이 상태 일부는 컴포넌트 C가 스스로 관리해도 문제가 없습니다. 즉, 각각의 상태에 대해 각 컴포넌트가 자율적으로 움직일 수 있습니다. 지역 상태 관리를 컴포넌트 C로 옮기면 더이상 컴포넌트 트리를 따라 프로퍼티를 전달하지 않아도 됩니다.

          +----------------+
          |                |
          |       A        |
          |                |
          |                |
          |    Stateful    |
          +--------+-------+
                   |
         +---------+-----------+
         |                     |
         |                     |
+--------+-------+    +--------+-------+
|                |    |                |
|                |    |                |
|       B        |    |                |
|                |    |                |
|                |    |                |
+----------------+    +--------+-------+
                               |
                      +--------+-------+
                      |                |
                      |                |
                      |                |
                      |                |
                      |                |
                      +--------+-------+
                               |
                      +--------+-------+
                      |                |
                      |                |
                      |        C       |
                      |                |
                      |     Stateful   |
                      +----------------+

컴포넌트 A의 상태도 덩달아 깔끔해졌습니다. 이 컴포넌트는 필요에 따라 자신의 상태와 가장 가까운 자식 컴포넌트의 상태만 관리하게 됩니다.

React에서 상태 옮기기는 다른 방향, 즉 상태 위로 옮기기도 가능합니다. 부모 컴포넌트인 컴포넌트 A와 자식 컴포넌트인 컴포넌트 B, C로 다시 돌아와서 살펴봅니다. A, B, C 사이에 얼마나 많은 컴포넌트가 있는지 상관 없습니다. 하지만 이번에는 컴포넌트 C가 이미 자신의 상태를 관리하고 있습니다.

          +----------------+
          |                |
          |       A        |
          |                |
          |                |
          |    Stateful    |
          +--------+-------+
                   |
         +---------+-----------+
         |                     |
         |                     |
+--------+-------+    +--------+-------+
|                |    |                |
|                |    |                |
|       B        |    |                |
|                |    |                |
|                |    |                |
+----------------+    +--------+-------+
                               |
                      +--------+-------+
                      |                |
                      |                |
                      |        C       |
                      |                |
                      |    Stateful    |
                      +----------------+

만약 컴포넌트 B가 C에서 관리하는 상태가 필요하다면 어떻게 해야 할까요? 이 상황에서는 공유할 수 없습니다. 상태는 프로퍼티 형태로 아래로만 넘겨줄 수 있기 때문인데요. 이런 이유에서 상태 계층을 위로 이동시킬 필요가 있습니다. 컴포넌트 C의 상태를 컴포넌트 B와 C가 공통으로 갖는 부모 컴포넌트의 위치로 올릴 수 있습니다. (여기서는 A가 해당되겠군요.) 만약 C가 관리하는 상태를 B에서 필요로 한다면 C는 상태 없는 컴포넌트가 됩니다. 상태는 A에서 관리되며 B와 C에 공유됩니다.

          +----------------+
          |                |
          |       A        |
          |                |
          |                |
          |    Stateful    |
          +--------+-------+
                   |
         +---------+-----------+
         |                     |
         |                     |
+--------+-------+    +--------+-------+
|                |    |                |
|                |    |        +       |
|       B        |    |        |Props  |
|                |    |        v       |
|                |    |                |
+----------------+    +--------+-------+
                               |
                      +--------+-------+
                      |                |
                      |                |
                      |        C       |
                      |                |
                      |                |
                      +----------------+

상태를 위로, 또는 아래로 옮기는 전략에서 단순 React를 사용할 때는 어떻게 상태 관리를 확장하는지 배울 수 있습니다. 더 많은 컴포넌트가 특정 상태에 관심을 가져야 하는 경우에은 상태에 접근해야 하는 컴포넌트 간의 공통 부모 컴포넌트까지 거슬러 올라가 상태를 둬야 합니다. 덧붙여 지역 상태 관리에서 충분히 관리할 수 있다면 컴포넌트는 필요한 만큼 상태를 관리하고 있기 때문입니다. 만약 컴포넌트 자체나 자식 컴포넌트에서 사용하지 않는 상태가 있다면 그 상태는 상태가 필요한 컴포넌트의 위치로 이동해야 합니다.

React의 상태 들어 올리기는 공식 문서에서 더 자세히 살펴볼 수 있습니다.

React의 고차 컴포넌트

고차 컴포넌트 (Higher order components, HOCs)는 React의 고급 패턴입니다. 이 패턴은 추상적인 기능이 필요할 때 사용할 수 있으며 여러 컴포넌트에서 선택적으로 기능이 필요할 때 활용할 수 있습니다. 고차 컴포넌트는 컴포넌트를 받아서 선택적 설정을 입력으로 받아 강화된 버전의 컴포넌트를 반환합니다. 이 기능은 JavaScript의 고차 함수 원칙인 함수를 반환하는 함수처럼 구현되었습니다.

만약 고차 컴포넌트가 익숙하지 않다면 React의 고차 컴포넌트 안내를 읽어보길 추천합니다. 이 글은 React의 고차 컴포넌트를 React의 조건부 렌더링의 용례와 함께 설명합니다.

고차 컴포넌트는 뒤에서 더 중요해지는데 Redux와 같은 라이브러리를 사용하게 되면 마주하게 되기 때문입니다. Redux 같은 라이브러리는 React의 뷰 계층(view layer)와 라이브러리의 상태 관리 계층과 "연결"하게 되며 이 과정에서 고차 컴포넌트를 사용해 처리하게 됩니다. (고차 컴포넌트로 이뤄지는 연결은 react-redux를 사용합니다.)

MobX와 같은 다른 상태 관리 라이브러리도 동일한 방식으로 적용합니다. 고차 컴포넌트는 라이브러리에서 제공하는 상태 관리 계층과 React의 뷰 계층을 붙이는데 사용합니다.

React의 Context API

React의 context API는 드물게 사용됩니다. 이 API를 사용하라 충고하지 않는 편인데 이 API는 안정적이지 않고 애플리케이션의 묵시적 복잡도(implicit complexity)를 높이기 때문입니다. 하지만 어떤 기능을 하는지 들어보면 왜 이런 기능이 있는지 충분히 이해할 수 있을 겁니다.

왜 이 기능을 알아야 할까요? React의 context는 컴포넌트 트리에서 속성을 묵시적으로 전달할 때 사용됩니다. 부모 컴포넌트에서 속성을 context로 선언하면 컴포넌트 트리 아래에 있는 자식 컴포넌트에서 활용할 수 있습니다. 명시적으로 각각의 컴포넌트 계층에 일일이 전달할 필요 없이 단순히 부모-자식 관계라면 부모 컴포넌트가 생성한 context를 자식 컴포넌트가 집어 사용할 수 있습니다. 모든 컴포넌트 트리에 걸쳐 언제든 꺼내서 쓸 수 있는, 보이지 않는 컨테이너가 존재합니다. 이 컨테이너 덕분에 컴포넌트에서 필요하지 않는 프로퍼티는 접근할 일이 없어지기 때문에 React에서 "프로퍼티 내려꽂기(props drilling)"라고 하는 일을 피할 수 있게 됩니다. 다시 원래 주제로 돌아와서 왜 이런 API를 알아야 할까요?

Redux나 MobX와 같은 세련된 상태 관리 라이브러리를 사용하다보면 어떤 시점에서 상태 관리 계층을 React 뷰 계층에 붙여야 하는 상황이 생깁니다. React의 고차 컴포넌트를 언급한 이유가 여기에 있습니다. 이 붙이는 과정을 통해 상태에 접근하고 수정할 수 있게 됩니다. 상태 자체는 일종의 상태 컨테이너 안에서 관리됩니다.

하지만 어떻게 모든 컴포넌트에서 이 상태 컨테이너에 접근할 수 있도록 붙일 수 있을까요? 이런 상황에서 React의 context를 사용할 수 있습니다. 최상위 컴포넌트 즉, React의 루트 컴포넌트(root component)에서 상태 컨테이너를 context로 지정합니다. 그래서 컴포넌트 트리에 있는 모든 컴포넌트에 명시적으로 전달하지 않으면서도 모두 접근할 수 있게 됩니다. 이 모든 과정은 React의 프로바이더 패턴으로 적용할 수 있습니다.

물론 이런 방식을 사용한다는 게 Redux 같은 라이브러리를 사용할 때마다 React의 context를 직접 제어해야 할 필요가 있다는 의미는 아닙니다. 이런 라이브러리는 이미 모든 컴포넌트에서 상태 컨테이너에 접근 가능하도록 모든 기능이 함께 제공되고 있습니다. 하지만 이 기능이 보이지 않는 곳에서 어떤 방식으로 동작하고 있는지 이해하게 된다면 여러 컴포넌트에서 상태를 제어하면서 도대체 이 상태 컨테이너는 어디서 오는 것일까 걱정할 필요도 없어지게 됩니다.

React의 상태 컴포넌트

React는 두 종류의 컴포넌트 선언이 존재합니다. ES6 클래스 컴포넌트와 함수형 상태 없는 컴포넌트(functional stateless component)입니다. 함수형 상태 없는 컴포넌트는 props를 인자로 받고 JSX를 반환하는 단순한 함수입니다. 이 함수는 어떤 상태도 갖지 않으며 React의 생애주기(lifecycle) 메소드에도 접근하지 않습니다. 이 컴포넌트는 이름 붙여진 그대로 상태가 없습니다.

function Counter({ counter }) {
  return (
    <div>
      {counter}
    </div>
  );
}

반면, ES6 클래스 컴포넌트는 지역 상태와 생애주기 메소드를 사용할 수 있습니다. 이 컴포넌트는 this.statethis.setState() 메소드에 접근 가능합니다. ES6 클래스 컴포넌트는 상태 컴포넌트로 사용할 수 있다는 의미입니다. 물론 이 컴포넌트가 꼭 지역 상태를 사용해야 한다는 뜻은 아니며 상태 없는 컴포넌트로도 작성할 수 있습니다. 일반적으로 상태가 없는 ES6 클래스 컴포넌트라면 생애주기 메소드를 사용하기 위해 클래스 형태로 작성한 경우입니다.

class FocusedInputField extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    this.input.focus();
  }

  render() {
    return (
      <input
        type="text"
        value={this.props.value}
        ref={node => this.input = node}
        onChange={event => this.props.onChange(event.target.value)}
      />
    );
  }
}

결론적으로 ES6 클래스 컴포넌트만 상태를 가질 수도 가지지 않을 수도 있습니다. 함수형 상태 없는 컴포넌트는 항상 상태가 없습니다.

덧붙여 고차 컴포넌트도 React 컴포넌트에 상태를 덧붙일 수 있습니다. 상태를 관리하기 위해 직접 고차 컴포넌트를 만들거나 recompose와 같은 라이브러리에서 제공하는 withState 고차 컴포넌트를 사용하는 것도 가능합니다.

import { withState } from `recompose`;

const enhance = withState('counter', 'setCounter', 0);

const Counter = enhance(({ counter, setCounter }) =>
  <div>
    Count: {counter}
    <button onClick={() => setCounter(n => n + 1)}>Increment</button>
    <button onClick={() => setCounter(n => n - 1)}>Decrement</button>
  </div>
);

고차 컴포넌트를 사용하면 어떤 컴포넌트에든 지역 상태를 추가할 수 있습니다.

컨테이너와 프레젠터 패턴

컨테이너와 프레젠터 패턴은 Dan Abramov의 블로그 포스트 이후 유명해졌습니다. 이 패턴에 익숙하지 않다면 지금이 살펴 볼 차례입니다. 컴포넌트를 컨테이너와 프레젠터로 구분합니다. 컨테이너 컴포넌트는 어떻게 동작하는가를, 프레젠터 컴포넌트는 어떻게 보이는가를 정의합니다. 컨테이너 컴포넌트는 ES6 클래스 컴포넌트로 구현되어 지역 상태를 관리합니다. 프레젠터 컴포넌트는 함수형 상태 없는 컴포넌트로 작성하여 프로퍼티로 받은 내용을 표현하고 부모 컴포넌트로부터 받은 함수 몇 가지를 실행하는 역할을 합니다.

Redux로 뛰어들기 전에 이 패턴 뒤에 있는 원칙을 이해할 필요가 있습니다. 상태 관리 라이브러리는 컴포넌트를 상태와 "연결"해줍니다. 상태 관리 계층과 연결된 컴포넌트를 **연결된 컴포넌트(connected component)**라는 용어로 부르기도 합니다. 이 컴포넌트는 어떻게 보이는가는 신경쓰지 않지만 어떻게 동작하는가에 집중하게 됩니다. 이런 컴포넌트가 바로 컨테이너 컴포넌트의 역할을 합니다.

MobX 아니면 Redux?

모든 상태 관리 라이브러리를 통틀어 Redux가 가장 유명하고 MobX는 살펴 볼 가치 있는 대안입니다. 두 라이브러리는 다른 철학과 프로그래밍 패러다임을 따라가고 있습니다.

둘 라이브러리 중 하나로 고르기 전에 이 글에서 설명한 내용에 대해 이해하고 있어야 합니다. 기본 React의 상태 관리를 확장하기 위해 다른 개념을 적용할 정도로 지역 상태 관리에 익숙해야 합니다. 미래에 규모가 커질 애플리케이션을 염두해서 상태 관리도 확장해야 한다는 점을 기억합시다. 상태 관리 위치를 변경하거나 React의 프로바이더 패턴을 활용해 context를 사용하는 것으로 이미 어느 정도 문제를 해결할 수 있어야 합니다.

Redux나 MobX를 사용하기로 결정했다면 Redux 또는 MobX: 혼란을 해결하려는 시도에서 더 심층적으로 다루고 있으니 읽어보기 바랍니다. 두 라이브러리를 비교하고 어떻게 적용할지 설명하고 있습니다. 아니면 React + Redux 학습 팁으로 바로 Redux를 시작할 수 있습니다.

이 글이 상태 관리 라이브러리를 사용하기 전에 어떤 역할을 하는지 이해하는데 도움이 되었으면 좋겠습니다. Redux와 MobX에 대해 더 궁금하다면 전자책/코스인 Taming the State in React를 확인해보세요.


2018-11-24: Rinae님의 피드백으로 프로퍼티 구멍내기를 프로퍼티 내려꽂기로 수정했습니다. 피드백 감사드립니다.

wordpress를 사용해서 운영하고 있던 블로그를 gatsby 기반으로 전환했다. 전환하는 과정에서 고려한 부분과 생각한 점을 정리하려고 한다.

전환 이전의 상황

그동안 wordpress를 직접 호스팅해서 사용하고 있었는데 매월 digitalocean에 비용을 내고 있는게 조금 아까웠다. 예전에는 블로그 외에도 이것 저것 올렸던 사이트가 있어서 크게 고민이 없었지만 이제는 다 정리되고 블로그만 남아 있었다. 게다가 거의 방치되다시피 하다보니 보안 문제도 계속 신경이 쓰일 수 밖에 없었다. wordpress를 실무에서 더이상 사용하고 있지 않아서 무슨 문제가 생기더라도 예전만큼 빠르게 알고서 대응할 수 없을 거란 생각도 들었다.

이런 이유에서 정적 사이트 생성기로 전환할 생각은 오래 전부터 했었다. 비용도 거의 무료인 데다 보안에도 크게 걱정할 필요가 없었다. 하지만 실천으로 옮기기 쉽지 않았다. 전적으로 마음에 드는 도구도 찾기 힘들었다. 블로그에 촛점을 맞춘 도구가 대부분이라 유연함이 상대적으로 부족했다. 또한 데이터 마이그레이션에 대한 두려움이 있었다. 모든 자료를 문제 없이 옮기더라도 문제가 있는지 없는지 검증할 필요가 있었다. 자동으로 추출하는 도구를 사용하더라도 분명 손이 가는 부분이 있을 것 같았다.

그리고 wordpress는 최고다. 그저 클릭 몇 번으로 새로운 기능을 쉽게 설치할 수 있고 테마도 손쉽게 바꿀 수 있는 데다 웹엔드에 앱도 제공한다. wordpress에서 벗어나고 싶어도 게으름이 내 발목을 꼭 잡고 놓아주질 않았다! 그래서 매번 "바꿀까?" 생각이 들 때마다 얼마 후에 "굳이 그래야 할까?"로 자연스럽게 의식의 변화가 생겼다.

I regret everything
모든 걸 후회합니다ㅏㅏ

쉬고 있는 동안 글은 안쓰는데 비용 나가는 것이 눈에 보이니 지금이라도 서둘러 옮겨야겠구나 하고 도구부터 찾았다.

도구 찾기

맨 처음엔 nextjs를 사용하려고 했었는데 생각보다 구현이 단순해서 직접 만져야 하는 부분이 많았다. 특히 파일 구조가 아닌 내가 원하는 url 구조를 넣으려니 생각보다 시간이 걸렸다. 한 3일 정도 만졌는데 뭔가 알 수 없는 404 오류가 계속 콘솔에 찍혀서 시간을 허비하고 있었다. 그러던 중 gatsby가 생각나서 튜토리얼만 봐야지 했는데 직접 만들려고 했던 기능들이 이미 다 훨씬 멋지게 제공되고 있었다.

특히 graphql로 쉽게 filesystem도 불러오고 사용할 수 있도록 구현되어 있고 url 구조도 내가 원하는 대로 생성할 수 있다는 점에 바로 마음을 돌렸다.

개발 과정

데이터 추출하기

Gatsby는 마크다운으로 작성되기만 하면 활용에 큰 문제가 없도록 구현되어 있어서 특히 편리했다. 그래서 hugo용이더라도 상관이 없어서 wordpress-to-hugo-exporter 플러그인을 사용해 추출했다.

데이터를 추출하기 전에 haruair.com에서 어떤 주소였는지 frontmatter에 history로 저장하도록 플러그인을 수정했다. 그리고 기존에 사용하던 headline도 저장했다.

글 몇 개를 제외하고 변환되긴 했는데 마크다운이 아닌 html로 저장된 문서도 몇 발견할 수 있었다. 지금도 다 확인하지 못해서 소셜 embed가 깨지는 페이지가 좀 있다. 변환이 안된 글도 있는데 플러그인에서 yaml 변환 과정에 문제가 있으면 깨진다. 누락되지 않는 페이지가 있는지 뒤늦게 확인했고 몇 개 안되어 손으로 변환했다.

영어와 한국어를 분리하고 싶어서 lang도 추가했다.

구현 사항과 사용한 플러그인

  • URL 처리: createPage()에서 path만 값만 넣으면 손쉽게 커스텀 경로를 사용할 수 있었다. frontmatter에 추가한 langtype, slug를 조합해서 사용하도록 했다. 또한 절대 경로도 사용할 수 있도록 url도 추가했다.
  • RSS 피드: gatsby-plugin-feed을 사용하면 쉽게 만들 수 있다. 다만 정적 사이트라서 xml 확장자를 써야만 하는 점이 아쉽다. /feed.xml/ko/feed.xml을 분리해서 생성하도록 config에 추가했다. 각각 어느 피드인지 title도 넣었다.
  • sitemap: 구글 웹마스터 도구에 제출하기 위해서 gatsby-plugin-sitemap을 추가했다.
  • PWA용 기능: gatsby-plugin-offlinegatsby-plugin-manifest를 추가했다. PWA는 잘 몰라도 속도가 빨라진 것은 바로 체감할 수 있었다.
  • 문법 강조: prismjs를 설치했다. 항상 hightlightjs를 사용해와서 조금 어색하긴 하지만 마음에 든다.
  • 소셜 공유: react-helmet으로 og meta와 twitter meta를 추가했다. 플러그인이 있을 것 같은데 일단 helmet으로 해결했다. 테스트 해보니 큰 문제 없었다.
  • 404 페이지 링크 문제: 내 코드에 문제인가 한참 씨름했는데 gatsby의 버그였고 현재는 고쳐졌다.
  • 배포: 도메인은 cloudflare에 있었고 github pages에 배포하려고 생각했는데 netlify가 정말 멋지게 서비스를 만들어둬서 netlify를 사용하게 되었다.
  • Google Analytics 설정: gatsby-plugin-google-analytics로 간편히 설정했다.

redirect과 canonical

앞서 자료를 추출하며 추가한 history로 redirect.json을 생성했다. 이 파일로 haruair.com에서 yoast SEO의 canonical 란에 새 주소를 저장했다.

<?php
if (php_sapi_name() !== 'cli') exit;
require_once(__DIR__ .'/wp-load.php');

$filename = './redirect.json';
$key = '_yoast_wpseo_canonical';

$data = json_decode(file_get_contents($filename));

foreach($data as $r) {
  $u = explode('/', $r->from);
  $id = array_pop($u);
  if (!is_numeric($id)) continue;

  $canonical = get_post_meta($id, $key);
  if ($canonical !== null) continue;

  update_post_meta($id, $key, $r->to);
  echo "SET {$id} {$r->to}". PHP_EOL;
}

사용하고 있는 amp 플러그인에서는 yoast의 canonical을 불러오고 있지 않아서 다음처럼 추가했다.

<?php
function haruair_amp_canonical($data, $post) {
  $data['canonical_url'] = get_post_meta($post->ID, '_yoast_wpseo_canonical', true);
  return $data;
}

add_filter('amp_post_template_data', 'haruair_amp_canonical', 100, 2);

여기까지 하고 Google Webmasters에 sitemap을 제출했다.

amp 페이지는 캐싱 되는 주기가 길어서 빨리 반영이 되지 않는 데다 사용하고 있던 cloudflare의 캐시를 매번 지워줘야 해서 확인이 어려웠다.

게다가 canonical을 추가하니 기존 사이트가 검색에서 사라졌는데 새 페이지가 아직 구글에 크롤링 되지 않아 아예 검색 결과에 나오지 않는 문제가 있었다. 그래서 모든 페이지에서 301 Moved Permanently로 새 주소에 이동하도록 코드를 추가했다.

<?php
function haruair_redirect_canonical() {
  if (!is_single()) return;
  $canonical = get_post_meta(get_the_ID(), '_yoast_wpseo_canonical', true);
  if ($canonical) {
    wp_redirect($canonical, 301);
    exit;
  }
}

add_action('template_redirect', 'haruair_redirect_canonical');

이제 색인이 어느 정도 정리되면 haruair.com 도메인도 netlify에서 제공하는 _redirect로 변경할 예정이다.

edykim.com

그동안 haruair.com을 오래 사용했는데 하나로 다 통합하기로 결정했다. 새 도메인에서 다시 시작하는 기분으로 깔끔하게 정리했다. 예전에는 블로그 구조에서 벗어나기 어려워서 노트처럼 사용하려는 취지와 조금 멀어졌는데 여기서는 좀 더 차분하게 내 자료를 쌓아갈 수 있으면 좋겠다.

처음 사용해본 Gatsby도 마음에 들었다. 주변에서도 많이 언급해서 궁금했었는데 이 정도로 만족스러울 줄은 몰랐다. Gatsby를 사용하면서 source와 transformer, plugin의 구분으로 확장의 성격을 명확하게 분리하는 점과, GraphQL로 마치 데이터를 가져와 페이지를 생성하면서도 그 과정 어느 순간이든 사용자가 개입할 수 있도록 열린 구조가 너무 좋았다. 작업 하면서 사소하지만 작은 코드를 기여하기도 했는데 기여 방법도 어떤 과정으로 참여할 수 있는지 잘 정리되어 있어서 손쉽게 제출할 수 있었다. 아직 코드가 지저분한데 정리도 하면서 gatsby도 좀 더 깊이 살펴보는 기회로 삼고 싶다.

HUZZAH!!!
어예! 공부다 공부 ✌

아직도 개인 신상이 정리가 되질 않아서 제출한 서류 결과만 기다리고 있는 상황이라 조금 답답했었다. 그래도 오랜만에 코드 만지면서 예전 생각도 들고, 얼른 다시 코드 짜는 삶으로 돌아가고 싶어졌다. 연말까지는 큰 일 없이 이렇게 지내게 될 것 같은데 조금 더 멀리 보고 부지런히 준비해야겠다.

이 가이드라인은 California Institute of Technology의 강의 자료인 C++ Operator Overloading Guidelines를 번역한 글로 C++에서 연산자를 오버로딩 할 때 유의해야 하는 부분을 잘 설명하고 있다.


C++ 연산자 오버로딩 가이드라인

사용자 정의 클래스를 사용할 때 연산자에 특별한 의미를 부여할 수 있다는 점은 C++의 멋진 기능 중 하나입니다. 이 기능을 연산자 오버로딩(operator overloading) 이라고 합니다. C++의 연산자 오버로딩은 클래스에 특별 멤버 함수를 다음과 같은 명명 규칙에 따라서 작성해 구현할 수 있습니다. + 연산자를 클래스에 오버로드 하는 것으로 예를 들면 operator+라는 이름의 멤버 함수를 클래스에 작성해서 제공할 수 있습니다.

다음은 사용자 정의 클래스에 일반적으로 오버로드하는 연산자 목록입니다.

  • = (할당 연산자, assignment operator)
  • + - * (이진 산술 연산자, binary arithmetic operators)
  • += -= *= (복합 할당 연산자, compound assignment operators)
  • == != (비교 연산자, comparison operators)

이 연산자를 구현하는데 있어 몇 가지 지침이 있습니다. 이 지침을 따르는 것은 매우 중요하며 각 지침은 꼭 버릇으로 만들기 바랍니다.

할당 연산자 =

할당 연산자는 다음과 같은 시그니처(signature)를 사용합니다.

class MyClass {
  public:
  ...
  MyClass & operator=(const MyClass &rhs);
  ...
}

MyClass a, b;
...
b = a;   // b.operator=(a); 와 동일함

= 연산자가 우측에 할당한 내용을 상수 참조로 받는 점을 확인할 수 있습니다. 이렇게 하는 이유는 명확한데 할당 연산자 왼쪽에 있는 내용을 바꾸고 싶은 것이지 할당 오른쪽은 변경을 원하지 않기 때문입니다.

또한 할당 연산자도 참조로 반환하는 점을 확인할 수 있습니다. 이 방식으로 **연산자 연결(operator chaining)**이 가능합니다. 원시 형식(primitive types)이 다음처럼 동작하는 것을 봤을 겁니다.

int a, b, c, d, e;

a = b = c = d = e = 42;

컴파일러는 이 코드를 다음처럼 해석합니다.

a = (b = (c = (d = (e = 42))));

다시 말하면 할당은 **우측 연관(right-associative)**입니다. 마지막 할당 연산이 먼저 평가되며 연쇄적인 할당에 따라 좌측으로 퍼져가게 됩니다. 특히,

  • e = 42는 42를 e에 할당하고 그 결과로 e를 반환합니다.
  • e의 값이 d에 할당되며 그 결과로 d를 반환합니다.
  • d의 값이 c에 할당되며 그 결과로 c를 반환합니다.
  • 기타 등등…

이제 연산자 연결을 지원하기 위해서 할당 연산자는 반드시 어떤 값을 반환해야 합니다. 반환되어야 하는 값은 할당의 좌측 을 참조합니다.

반환 참조에는 상수로 선언되지 않았음 을 확인할 수 있습니다. 이 점은 좀 혼란스러울 수 있는데 다음과 같은 기괴한 코드를 작성할 수 있기 때문입니다.

MyClass a, b, c;
...
(a = b) = c;  // 이건 뭐죠??

이건 코드를 처음 봤다면 아마 이런 상황을 방지하기 위해 operator=를 상수 참조로 반환하고 싶을지도 모릅니다. 하지만 이런 구문(statements)은 원시 형식과 함께 동작할겁니다. 그리고 더 나쁜 점은 어떤 도구는 이런 동작 방식에 의존하고 있다는 점입니다. 그러므로 operator=상수가 아닌 참조로 반환하는 것이 중요합니다. 경험적으로 “상수에 괜찮다면 사용자 정의 자료 형식에도 괜찮다”고 말할 수 있습니다.

그래서 가상의 MyClass 할당 연산자를 다음처럼 작성할 수 있습니다.

// 우측 할당에서 상수 참조를 받습니다.
// 좌측에 상수가 아닌 참조를 반환합니다.
MyClass& MyClass::operator=(const MyClass &rhs) {
    ...  // 할당 연산을 수행합니다!

    return *this;  // 자기 자신을 참조를 반환합니다.
}

기억하세요. this는 이 객체에 대한 포인터며 멤버 함수로 호출되는 대상입니다. a = ba.operator=(b)처럼 취급되는 방식을 보면 반환 객체 자체가 호출되는 함수라는 점이 더 와닿을 것입니다. 객체 a는 좌측에 해당합니다.

하지만 멤버 함수는 객체에 대한 포인터를 반환하는 것이 아니라 객체에 대한 참조를 반환해야 합니다. 그래서 *this를 반환했는데 이 코드는 무엇을 가리키고 있는지를 반환했지 포인터 자체를 반환한 것이 아닙니다. (C++에서는 인스턴스는 참조로 전환되고 또 그 역으로도 성립하는데 거의 자동으로 처리됩니다. 그래서 *this는 인스턴스지만 C++는 암시적으로 인스턴스에 대한 참조로 변환합니다.)

이제 할당 연산자에 가장 중요한 점이 하나 더 있습니다. 자기 할당(self-assignment)를 꼭 확인해야 합니다!

클래스가 자체적으로 메모리 할당을 하는 경우라면 특히 중요합니다. 할당 연산자가 일련의 연산을 수행할 때 일반적으로 다음처럼 동작합니다.

MyClass& MyClass::operator=(const MyClass &rhs) {
    // 1. MyClass가 내부적으로 갖고 있는 모든 메모리를 할당 해제합니다.
    // 2. rhs의 내용을 보관하기 위해 메모리에 할당합니다.
    // 3. rhs로부터 값을 인스턴스에 복사합니다.
    // 4. *this을 반환합니다.
}

이제 이렇게 작성하면 다음과 같은 일이 일어납니다.

MyClass mc;
...
mc = mc;     // 짜잔!

이 코드를 보면 프로그램에 대혼란을 불러온다는걸 볼 수 있을겁니다. mc는 좌측에도 있고 또한 우측에도 있기 때문에 가장 먼저 일어나는 일은 mc가 내부적으로 들고 있는 모든 메모리를 해제합니다. 하지만 여기서 값이 복사될 위치이기도 합니다. 우측도 mc가 있기 때문이죠! 이제 나머지 할당 연산자 내부를 완전히 다 엉망으로 만들고 맙니다.

이런 문제를 손쉽게 피하려면 자기 할당을 확인합니다. “두 인스턴스는 같나요?”라는 질문에 답하는 방법은 많지만 우리 용도를 생각해보면 객체 주소가 동일한지 확인하는 정도면 지금 용도에 맞습니다. 만약 주소가 동일하면 할당을 하지 않습니다. 주소가 다르면 할당을 수행합니다.

이제 올바르고 안전한 버전의 MyClass 할당 연산자를 생각하면 다음과 같습니다.

MyClass& MyClass::operator=(const MyClass &rhs) {
    // 자기 할당을 확인합니다.
    if (this == &rhs)      // 동일 객체?
        return *this;        // 맞네요. 그럼 할당을 건너뛰고 *this를 반환합니다.

    ... // 할당 해제, 새 공간을 할당하고 값을 복사합니다...

    return *this;
}

또는 간단하게 다음처럼 할 수 있습니다.

MyClass& MyClass::operator=(const MyClass &rhs) {

    // `rhs`가 `this`와 다를 때만 할당을 합니다.
    if (this != &rhs) {
        ... // 할당 해제, 새 공간을 할당하고 값을 복사합니다...
    }

    return *this;
}

위 비교에서 this는 호출되는 객체 포인터고 &rhs는 인자로 전달된 객체를 가리키는 포인터라는 점을 기억한다면 위 코드와 같은 검사로 자기 할당의 위험성을 회피할 수 있다는 점을 확인할 수 있을겁니다.

결론을 정리하면 할당 연산자를 위한 가이드라인은 다음과 같습니다.

  1. 인자는 상수 참조로 받습니다. (할당 우측)
  2. 좌측에 참조를 반환해서 안전하고 적절한 연산자 연결을 지원합니다. (*this를 반환하는 방법으로)
  3. 포인터를 비교해서 자기 할당을 확인합니다. (this&rhs)

복합 할당 연산자 += -= *=

산술 연산자에 대해 특별히 언급할 부분이 있는데 뒤에서 자세히 다루겠습니다. 이 연산자는 비구조(destructive) 연산자라는 점이 중요합니다. 바로 할당의 좌측 값이 갱신되거나 대체되기 때문입니다. 다음 예시를 확인합니다.

MyClass a, b;
...
a += b;    // a.operator+=(b) 와 동일함

이 경우에는 += 연산에 의해 값이 수정되었습니다.

어떻게 이 값이 변경 되었는가는 그렇게 중요하지 않습니다. 명백하게 MyClass가 이 연산자의 의미가 어떤 것인지 나타내고 있기 때문입니다.

이런 연산자의 멤버 함수 시그니처는 다음과 같아야 합니다.

MyClass & MyClass::operator+=(const MyClass &rhs) {
    ...
}

rhs가 상수 참조인 이유는 이미 앞에서 다뤘습니다. 그리고 이런 연산자의 구현 또한 직관적입니다.

하지만 연산자가 MyClass 참조를 반환하는 것을 볼 수 있으며 상수가 아닌 형태로 반환하고 있습니다. 이 말은 다음과 같은 코드도 가능하다는 의미입니다.

MyClass mc;
...
(mc += 5) += 3;

누가 이런 코드를 작성하는지 저에게 물어보지 않기 바랍니다. 하지만 다른 일반적인 할당 연산자와 같이 이런 원시 데이터 형식에서는 허용되야 하는 방식입니다. 사용자 정의 데이터 형식도 원시 데이터 형식에서 이런 연산자와 함께 동작하는 방식과 맥락이 맞는 동작을 제공해야 모든 코드가 기대한 대로 동작할 것입니다.

이런 작업은 매우 직관적입니다. 단지 복합 할당 연산자 구현을 작성하고 *this를 마지막으로 반환하도록 작성합니다. 다른 일반적인 할당 연산자처럼 말이죠. 그러면 다음과 같은 코드를 작성하게 될 것입니다.

MyClass & MyClass::operator+=(const MyClass &rhs) {
    ...   // 복합 할당 작업을 처리합니다.

    return *this;
}

마지막으로 일반적으로 복합 할당 연산자더라도 자기 할당 문제에 유의해야 합니다. 운좋게도 C++ 트랙 연구실에서 이 걱정을 할 필요는 없겠지만 클래스를 직접 작성하고 사용할 때마다 이 문제에 대해 항상 생각하고 있어야 합니다.

이진 산술 연산자 + - *

이진 산술 연산자는 연산의 양쪽을 모두 수정하지 않으며 두 인자로 만든 새로운 값을 반환한다는 점에서 흥미롭습니다. 아마 이 작업은 조금 추가적인 작업을 필요로 해서 짜증날 수 있겠지만 여기에 비밀이 있습니다.

복합 할당 연산자를 활용해서 이진 산술 연산자를 정의하세요.

방금 제가 당신이 숙제에 쓸 수많은 시간을 절약해줬습니다.

이미 += 연산자를 구현했고 이제 + 연산자를 구현하려고 합니다. 함수 시그니처는 다음과 같습니다.

// 인스턴스의 값을 다른 곳에 추가하고 결과와 함께 새 인스턴스를 반환합니다.
const MyClass MyClass::operator+(const MyClass &other) const {
    MyClass result = *this;     // 자신의 사본을 만듭니다. `MyClass result(*this);`와 같습니다.
    result += other;            // 다른 곳에서 사본에 더하기 위해 +=를 사용합니다.
    return result;              // 모두 끝났습니다!
}

간단하네요!

사실 명시적으로 모든 과정을 다 설명했지만 원한다면 이 모든 코드를 다음처럼 한 줄 구문으로 작성할 수 있습니다.

// 인스턴스의 값을 다른 곳에 추가하고 결과와 함께 새 인스턴스를 반환합니다.
const MyClass MyClass::operator+(const MyClass &other) const {
    return MyClass(*this) += other;
}

이 코드는 *this의 사본으로 이름 없는 MyClass 인스턴스를 생성합니다. 그리고 += 연산자는 이 임시 값에서 호출하고 반환합니다.

마지막 구문이 아직 이해가 되지 않는다면 앞서 단계를 풀어서 설명한 코드를 사용하기 바랍니다. 하지만 정확히 무슨 과정이 이뤄지는지 이해된다면 짧은 코드를 사용하세요.

+ 연산자가 상수 인스턴스를 반환한 것이지 상수 참조를 반환하지 않았다는 점 을 알 수 있을 겁니다. 상수 참조를 반환하지 않는 것으로 다음과 같은 이상한 구문을 작성하는 일을 막습니다.

MyClass a, b, c;
...
(a + b) = c;   // 엥...?

이 구문은 기본적으로 아무 일이 일어나지 않습니다. 하지만 + 연산자가 상수가 아닌 값을 반환하는 경우에는 컴파일이 됩니다! 반환 값을 상수 인스턴스로 한다면 이런 광기는 더이상 컴파일 되지 않을 것입니다.

이진 산술 연산자를 위한 지침을 정리하면 다음과 같습니다.

  1. 복합 할당 연산자를 처음부터 구현합니다. 그리고 이진 산순 연산자를 복합 할당 연산자를 사용해서 이진 산술 연살자를 정의합니다.
  2. 허용되지 말아야 할 쓸모 없고 혼란스러운 할당 연산을 방지하기 위해서 상수 인스턴스를 반환합니다.

비교 연산자 ==, !=

비교 연산자는 매우 간단합니다. 다음 함수 시그니처를 사용해서 ==를 먼저 정의합니다.

bool MyClass::operator==(const MyClass &other) const {
    ...  // 값을 비교하고 bool 형식으로 결과를 반환합니다.
}

구현 내부는 매우 명확하고 직관적입니다. bool 반환 값도 아주 분명합니다.

여기서 중요한 점은 != 연산자도 == 연산자를 사용해서 간편하게 정의할 수 있습니다. 다음과 같이 작성하세요.

bool MyClass::operator!=(const MyClass &other) const {
    return !(*this == other);
}

== 연산자를 구현하려고 고생해서 만든 코드를 그대로 재사용하는 방법입니다. 또한 ==!= 구현이 서로를 구현하고 있기 때문에 이 연산자 사이에서 불일치 문제가 발견될 가능성이 매우 낮습니다.


Updated Oct 23, 2007

Copyright (c) 2005-2007, California Institute of Technology.


더 읽을 거리

Bjarne Stroustrup 책을 읽고 있는데 단순하면서도 마음에 남는 말이 참 많다.

Our code must be maintainable … A successful program “lives” for a long time (often for decades) and will be changed again and again. … Only a failed program will never be modified.

코드는 꼭 유지보수가 가능해야 합니다. … 성공적인 프로그램은 오랜 시간을, 종종 수십 년을 “살며” 계속 변경됩니다. … 실패한 프로그램은 절대 수정되는 일이 없습니다.

To be maintainable, a program must be simple relative to its requirements, and the code must directly represent the ideas expressed.

유지보수가 가능하려면 프로그램은 프로그램의 요구사항에 비해 단순해야 하며 코드는 아이디어의 표현을 직접 드러내야 합니다.

A program is a precise representation of our understanding of a topic.

프로그램은 우리가 주제를 어떻게 이해하는지를 알려주는 정확한 표현(representation)입니다.

Think about the problem itself, rather than your incomplete solution.

자신의 불완전한 해결책보다 문제 자체에 대해 생각하세요.

Programming is part practical, part theoretical. If you are just practical, you will produce non-scalable, unmaintainable hacks. If you are just theoretical, you will produce unusable (or unaffordable) toys.

프로그래밍은 실용적인 부분과 이론적인 부분이 있습니다. 만약 당신이 실용적이기만 한다면 당신은 확장하기 어렵고 유지보수 하기 어려운 코드 덩어리를 만들 겁니다. 반대로 이론적이기만 하면 사용 불가능한 (또는 감당하기에 너무 비싼) 장난감을 만들게 됩니다.


이 챕터 끝에 길을 잃은 기분이 들 때 이 책을 꺼내서 이 챕터를 다시 읽으라는 말에 울컥했다. 요즘 앞으로 어떤 개발을 해야 할지, 어떤 시장에서, 환경에서, 어떤 언어로 할지 고민이 많다. 다시 처음부터 시작하는 기분에 더 갈피가 잡히지 않는 것 같다.

웹 외의 환경은 해본 적이 없기도 하고 학교 가면 하게 될 것 같아서 cpp를 한동안 볼 것 같다. 영어도, 수학도 준비해야 하고. 적어보니 바빠야 하는데 막상 하나 제대로 하지 못하는 것 같고. 이렇게 고민에 고민을 더한다.

내 기분 속여서 하려고 노력하기보다 어떻게든 꾸역꾸역 해야 할 때가 온 것 같다. 빨리 방향도 생각도 다듬어졌으면 좋겠다!

C. Titus Brown의 글 A framework for thinking about Open Source Sustainability?을 번역했다. 공유지의 비극과 그 해결책을 오픈소스 프로젝트에 적용해보는 글인데 오픈소스 거버넌스에서 기성 연구를 적용하는 글이라 더 흥미롭다.

오픈소스 지속가능성 체계적으로 생각해보기

공유자원 문제를 온라인 오픈 프로젝트에 적용할 수 있을까?

Nadia Eghbal의 아름다운 글인 “공유지의 비극”을 다시 읽었습니다. Elinor Ostrom의 공유자원 연구에 기반한 대안적 결과를 읽으며 여러 맥락에서 다양한 생각이 들었고 그 생각을 공유하려고 합니다.

Nadia는 오픈소스의 지속가능성 문제에 대해 탐구하고 있습니다. 중요한 오픈소스 소프트웨어가 상대적으로 적은 인원으로 관리되고 있으며 금전적 보장 방법이 많지 않은 상황이라 중대한 문제로 볼 수 있습니다. 이 문제의 규모나 범위를 따지면 누구와 대화하고 있냐에 따라 달라집니다. Python의 과학적 컴퓨팅 환경에 관한 충격적인 그림을 보면 numpy는 겨우 6명의 메인테이너가 유지하고 있는 점을 확인할 수 있습니다. Python에서 과학적 컴퓨팅 환경을 생각해보면 numpy에 상당히 의존적이기 때문에 메인테이너의 수가 적다는 이야기는 쉽지 않은 도전처럼 들립니다. 소프트웨어 개발에 관한 일반적인 도전 과제를 심층적으로 살펴보고 싶다면 “The Astropy problem”, Muna et al., 2016를 보도록 합니다.

(이후 논의에서는 과학적 소프트웨어에 치중되어 있지만 더 넓은 범위에도 적용할 수 있다고 생각합니다.)

저는 생물정보학(bioinformatics) 분야에서 일하고 있습니다. 이 분야는 다양한 조합의 소프트웨어 패키지를 사용하며 이런 패키지도 대학원생이나 포스트닥터, 학부에 계신 분이나 직원 등 다양한 사람이 관리하고 있습니다. 제 랩에서도 소프트웨어를 개발하고 있습니다. (주로 khmersourmash를 개발하며 그 외에도 여럿 있습니다.) 그리고 지난 해에는 지속적으로 소프트웨어를 유지하기 위한 방법도 개발했는데 주로 테스트와 지속적 통합을 중심으로 동작합니다. 하지만 항상 무언가 제대로 동작하지 않는 부분이 있습니다. 자동화된 관리에서 정말 사소한 상수인데도 그렇습니다. 운이 좋다고 해야할지 Jupyter와 같은 프로젝트와는 다르게 저희 소프트웨어를 사용하는 사람은 그리 많지 않아서 버그 리포트와 관리 문제가 주체 못할 정도로 쏟아지진 않습니다.

이미 언급했듯 오픈소스 소프트웨어를 관리하는 상수는 꽤 노력이 필요합니다. 매주 소프트웨어의 어떤 부분이든 작업하다보면 버그와 관련되어 있지 않는 경우가 드뭅니다. 아니면 새로운 기능을 추가하기 위해 뒤집어 엎어야 하는 부분이 많은 경우도 있습니다. 물론 랩에서 저나 다른 사람이 갑자기 연구나 코딩에 영감이 온다면 한동안 그런 문제를 마주할 일이 없긴 할겁니다.

학습에 있어서도 동일하게 노력이 필요합니다. 매년 2주짜리 과정분석 워크샵을 진행해 새로운 접근법, 소프트웨어, 자료 타입 등 발전에 필요한 학습을 합니다. 2010년에 배운 지식도 여전히 동작하긴 하지만 대부분 지독하게 썩었을 것이라 예상합니다.

다년간 여러 패키지를 관찰해본 제 경험에서는 활발하게 관리되지 않는 오픈 온라인 프로젝트(소프트웨어와 학습 자료를 포함)는 빠르게 부식한다는 확고한 결론에 도달했습니다. 또한 사람들이 많이 사용하는 프로젝트는 고쳐야 할 많은 버그와 문제를 항상 찾게 됩니다. 이 관리된다는 말에는 다른 사람들이 당신의 온라인 프로젝트를 _향상_하는데 실제로 도움을 주고 싶어서 GitHub에 리뷰 받아 머지되거나 거절되야 할 풀 리퀘스트를 제출하는 것을 포함하지 않았습니다. 또는 (정말 성공적이라면) 어떤 회사에서 프로젝트에 참여해 그들의 코드를 넣고 싶어할지도 모르겠지만 그런 회사도 포함하지 않았습니다.

프로젝트가 활발하게 관리되려면 지속적인 관심이 필요합니다. 이 관심에는 멋진 과학적 소프트웨어 패키지의 제멋대로 자란 생태계, 실제 메인테이너로 구성되어 있지만 상대적으로 작은 커뮤니티도 포함됩니다. 이 모든 것이 하나로 섞이면 과학계에서 마주하는 오픈소스 지속 가능성 문제로 이어집니다. 영웅적인 노력 없이는 이 모든 프로젝트를 유지할 수 있는 인력이 없습니다. 과학계에서의 소프트웨어 유지라는 명확한 커리어 패스가 존재하지 않는다면 오픈소스 유지보수를 위해 더 많은 사람을 찾는 일은 도덕적이지도, 지속 가능하지도 않다는 점이 명확합니다.

최근에 동료와 다른 오픈소스 프로젝트에 대해 브레인스토밍을 한 적이 있습니다. (뒤에 더 설명합니다.) 이 과정에서 이 문제를 공유자원 문제라는 틀 안에서 생각해보려고 했습니다. 프로젝트의 성공이란 지속가능성 문제와 맡닿아 있으며 공유 자원 프레임워크를 사용하면 그 지속가능성에 대해 판단할 수 있을 거라 생각하고 이 틀을 찾기로 했습니다.

공유 자원, 공유지의 비극, 지속가능성을 위한 디자인 원리

제가 알고 있는 공유자원 프레임워크는 Elinor Ostrom의 책, 공유지의 비극을 넘어 (원제: Governing the Commons)에서 왔습니다. 이 책에서는 아주 일반적인 공유자원 문제를 설명하고 공동체가 이런 문제를 어떤 방식으로 해결했는지 논의하고 있습니다.

개괄적으로, 60, 70년대 Elinor Ostrom과 동료는 “공유지의 비극”이라는 유명한 개념을 설명했습니다. 공유지의 비극은 공유자원이 이기적인 이유로 과도하게 이용되는 상황을 의미합니다. 이 문제는 피할 수 있습니다. 꼭 정부의 규제나 기업화만 이런 문제를 관리하는 방법이 아닙니다. 실제로 공동체 대다수는 공유자원을 지역적으로 관리하는 방식을 통해 문제를 해결했습니다. Ostrom과 다른 사람의 사례 연구에서 Ostrom은 공유 자원의 지속 가능성을 위한 8가지 “디자인 원칙”을 추출했습니다.

Nadia의 블로그 포스트에서 잘 설명하고 있고, 또한 읽기 좋게 정리되어 있는 위키피디아에 있는 8가지 디자인 원칙 링크를 남겨둡니다.

Ostrom은 이 연구에 대한 업적으로 2009년 노벨상을 수상했습니다.

오픈 온라인 프로젝트로 돌아가기

저와 동료들은 이 프레임워크를 디지털 자원인 오픈 온라인 프로젝트에 어떤 방식으로 적용할 수 있을까 고민했습니다. 디지털 자원은 물리적 자원과 달리 소비할 수 없고 누가 프로젝트의 소스 코드를 소비한다고 해서 다른 사람이 사용하지 못하는 것이 아니기 때문입니다.

대화 속에서 답을 찾았고 그 답은 노력(effort)이었습니다. 오픈 온라인 프로젝트의 공유 자원은 노력입니다.

기여자가 프로젝트에 새로운 기능을 추가하려면 무슨 일을 하게 되나요? 노력을 들입니다. 기여자가 버그를 보고할 때는? 역시 노력을 들입니다. 좋은 버그 리포트를 남길 때는? 노력을 들입니다. 문서를 작성할 때는? 기능을 테스트할 때는? 새 기능을 추천할 때는? 모든 과정에서 노력이 필요합니다.

하지만 이보다 더 깊은 주제가 있습니다. 새로운 기여자를 프로젝트로 끌어들이면 노력의 크기를 성장시킬 수 있습니다. 오픈 소스 프로젝트에 금전적 지원을 줄 새 투자자와 만나게 될 때, 프로젝트에 얼마나 많은 헌신적 노력이 있는가에 따라 그 지원의 규모가 증가하기도 합니다.

물론 전에 썼던 것과 같이 모든 기여가 노력으로 가치 있는 것은 아닙니다. 어떤 기여는 그 기여가 주는 가치보다 프로젝트에 더 많은 _비용 부담_을 주기도 합니다. 예를 들면 새로운 기능 제안, 나쁜 버그 리포트가 있습니다. 기여가 없는 커다란 기능 추가는 단순히 리뷰하고 거절하는 일에서도 핵심 프로젝트 메인테이너에게 엄청난 비용적 부담을 줄 수 있습니다. 이런 일은 유지보수 필요성이 낮은 일인데 유지보수가 프로젝트의 노력을 죄다 끌어가야만 하는 상황으로 뒤바뀔 수도 있습니다.

Fernando Perez가 #GCCBOSC에서 비슷한 점을 언급한 적이 있습니다. 바로 오픈소스 프로젝트에서 사람을 채용해가는 방식에 대한 지적입니다. 많은 회사가 오픈소스 커뮤니티에서 사람을 채용합니다. 단순히 생각해봐도 오픈소스 커뮤니티에서 이들을 교육하는데 들인 노력은 안중에 없이 발굴해가는 것은 문제입니다.

공유 자원의 지속가능성을 위한 8가지 디자인 원칙을 보고 “노력”을 공유 자원으로 정의한다면 오픈소스 프로젝트에 다음처럼 적용할 수 있습니다.

  1. 오픈소스 프로젝트에 누가 기여자인지 명확하게 정의합니다.
  2. 오픈 온라인 프로젝트에서의 노력은 프로젝트의 필요에 따라 지역적으로 적용됩니다.
  3. 오픈 소스 프로젝트는 기여하는 사람이 디자인 결정에 참여한다는 규칙을 따릅니다.
  4. 크게 기여하는 사람을 공식적으로 프로젝트에 참여하도록 해서 결정에 더 큰 역할을 맡을 수 있도록 합니다.
  5. 커뮤니티의 규칙을 위반한 기여자는 제재할 수 있는 범위를 둡니다.
  6. 충돌 대부분은 법률적으로 해결하기보다 프로젝트 내부적으로 해결합니다.
  7. 충돌 대부분은 가벼운 방법과 논의로 다룹니다.
  8. 오픈소스 기여자 대부분은 여러 프로젝트에 기여합니다. 예를 들면 Python 생태계에서는 같은 사람이 많은 프로젝트에 참여하고 있습니다. 이런 맥락에서 Python 생태계를 큰 규모의 공유 자원으로 여기고 많은 지역적 공유 자원으로 구성되어 있다는 점을 고려해야 합니다. 예를 들어 “핵심 CPython 개발”, “수치 연산/numpy 라이브러리” 등의 지역적 공유 자원이 존재합니다.

제 생각에는 많은 종류의 열린 공동체에 일반적으로 적용할 수 있는 항목이라고 생각합니다.

그렇다면 이게 무슨 의미일까요?

저와 동료가 이런 방식으로 생각하기 시작한 후로 이 관점에서 오픈소스 프로젝트와 온라인 공동체 자료를 보기 시작했습니다. 이 틀은 오픈소스의 지속 가능성을 생각하는데 아주 간단하고 좋은 프레임워크가 될 수 있습니다. “얼마나 열리면 지나치게 열린 것인가” 포스트는 정확히 이 생각에서 온 글입니다. 또한 당신의 프로젝트에 사람들이 더 모집되는 것을 긍적적으로 봐야하는지 설명합니다. 프로젝트에 노력을 쓸 수 있는 양이 늘어난다면 프로젝트 필요에 더 많은 노력을 사용할 수 있습니다. 이렇게 참여를 늘린다는 점에서 행동 강령과 기여자 가이드라인이 더 중요해집니다. 장기적으로 참여할 사람을 찾고 노력할 사람을 유지하는게 가능하게 됩니다.

이 관점 자체로는 어떤 문제도 해결하지 않습니다. 하지만 이 원칙은 정말 좋은 사례 연구와 함께 묶여 있으며 공유 자원에 대한 연구로 어떻게 공동체 자원을 지속 가능하게 관리하는지 심도있게 다루고 있습니다.

좀 더 세부적으로 다루자면 일반적인 오픈 온라인 프로젝트를 한다는 맥락에서 몇 가지 고려해야 할 점을 제안하고 있습니다.

먼저 오픈소스 프로젝트에 사용할 노력이 잠식되지 않도록 지켜내야 합니다. 프로젝트가 성공하려면 기여 잠재력이 가용 노력에 얼마나 영향을 주는지 평가해야 합니다. 이런 평가가 기술적 기여에는 이미 포함되어 있겠지만 (“기술적인 빚”이란 관점에서), 버그 리포트와 기술 제안에서도 고려되어야 합니다. (물론 프로젝트 대다수는 이런 점을 이미 고려하고 있습니다.)

둘째, 지속적으로 유지보수 할 필요가 있는 부분(코드, 문서, 설치 등)에 들어가는 비용도 노력이 들어가는 부분으로 계산해야 합니다. 새 기능을 기여하면서 이런 유지보수가 필요한 부분을 간과하는 경우가 있으므로 세심히 살펴야 합니다. 새 기여자가 계속 도움을 주려고 하나요? 유지보수하기 위한 노력을 들여 헌신하려고 하나요? 그렇지 않다면 이런 유지보수에 노력을 더하게 되는 기여는 파트너십 등을 통해서 미뤄야 합니다.

셋째로, 새 기여자를 교육하고 육성하는 방식은 장기적인 관점에서 가용 노력을 늘린다는 점을 염두해야 합니다. 하지만 기여자의 심리는 까다롭고 교육하고 육성한다고 해서 계속 이 프로젝트에 남아 있으리라는 예측은 그렇게 간단하지 않습니다. Python 코어 멘토 프로그램처럼 어떤 프로젝트는 뛰어난 인큐베이터를 갖고 있습니다. 이런 곳은 자신들의 노력을 새로운 기여자를 찾는 일에 사용하는데 관심있는 사람들이 운영합니다. 제가 생각하기에는 친근한 환경을 만들고 기여하고 싶은 사람들에게 갖는 기대감이 어떤 방향인지 잘 알려줄 수 있다면 새로운 기여자에게 도움이 될거라고 봅니다. 우리 유지보수 노력에 기여하고 싶은 사람에게 어떻게 하면 새로운 기능도 추가하고 버그도 개선할 수 있는지 기쁘게 알려줄 수 있다는 그런 마음가짐 말이죠. 장기적으로 보면 건강한 공동체가 즉, 건강한 프로젝트라고 할 수 있습니다.

넷째로, 모든 또는 대다수의 자원 제공자가 의사 결정에 참여하도록 허용하는 흥미로운 구조의 영향을 받습니다. 이 부분은 좀 더 세세하게 살펴봐야 하긴 합니다. 하지만 대략적으로 설명하면 프로젝트에서 어느 정도 수준의 투자와 기여가 어떤 실효적 수준에서 정책이나 원칙 수립 단계의 보상을 받게 되는지 정할 필요가 있습니다.

다섯째로, 자금 제공자의 기대치를 설정하고 투자를 얻는 과정에서 프로젝트의 성숙도를 측정하는 기준을 정의하는 일이 도움이 될 수 있습니다. 제 경험에 따르면 대부분의 자금 제공자는 _프로젝트 지속 가능성_를 도식화 하는 일을 가장 우선시 합니다. 위에서 얘기한 디자인 원칙(그리고 공유 자원에서 나온 사례 연구)을 프로젝트 성숙도의 기반으로 삼는다면 지속 가능성을 판단할 수 있게 됩니다. 프로젝트의 자금 지원 제안을 작성한다고 가정하면 어느 디자인 원칙과 연계된 부분을 향상하려고 하며 어떻게 지속 가능 프레임워크와 연결되는지 적용할 수 있습니다. 예를 들어 “지금 당장 새로운 기여자가 참여하는데 있어 우리 역량을 걱정하고 있습니다. 또한 회사로부터 큰 규모의 기여를 하겠다는 제안을 받고 있습니다. 저희는 프로젝트 관리 방식을 구축하고 기여자를 위한 안내를 향상해 새 기여자와 투자자에게 어떤 수준의 투자와 기여를 기대하고 있는지 명확하게 설명하려고 합니다.” 식으로 설명한다면 자금 제공자는 이런 설명에 만족할겁니다.

“노력”이라는 공유 자원 프레임워크가 오픈소스 프로젝트와 오픈 온라인 프로젝트에 일반적으로 정말 잘 맞나요?

좋은 질문입니다. 저는 공유 자원에 대해 해박하지 않고 앞으로도 읽어야 할 것이 정말 많습니다. 공유 자원 프레임워크와 부합하지 않는 부분도 보여서 좀 더 다듬어야 하는게 사실입니다. 하지만 오픈소스 프로젝트가 동작하는 방식과 잘 맞아 떨어진다는 게 제 직감입니다. 공유 자원과 같은 개념적인 틀은 제 관점을 다시 생각하게 하고 더 나은 그림에 맞춰볼 수 있게 하며 문제를 해결하는데 다른 결론에 닿게 할 지도 모르겠습니다. 이 예제는 앞서 언급했던 “얼마나 열리면 지나치게 열린 것인가” 포스트에서 찾아볼 수 있습니다.

제가 정말로 시도하고 싶은 일은 오픈 소스 프로젝트의 사례 연구에 참여하여 오픈소스 프로젝트가 실제 삶에서 어떻게 작동하는지 연구하며 이 프레임워크에 부합하는지, 혹은 부합하지 않는지 확인하는 일입니다. 몇 년 내로 안식년을 가지게 되는데 그 기회가 될지 모르겠습니다.

이 프레임워크에서 가장 좋아하는 점 하나는 오픈소스 프로젝트에서 자유 오픈소스 소프트웨어에서 큰 가치로 여겼던, 흐릿해진 목표와 분리해서 생각할 수 있게 되었다는 점입니다. “우리는 행복한 대가족이고 모든 부분이 함께 동작합니다!” 라는 구호는 메인테이너의 삶이 열린 기여에 의해 망가지는 상황을 마주하면서 희미해졌습니다. 오픈소스 프로젝트는 오늘날 세계를 구성하는 커다란 부분을 실제로 운영하고 있으며 지속 가능성을 포함한 어떤 관점에서도 공식적이지 않은 방식으로 운영했던 과거의 접근 방식으로는 더 이상 지속할 수 없다고 한 Fernado Perez의 이야기와 맥락이 닿습니다. 우리는 지속 가능한 오픈 프로젝트를 목표로 더 책임감을 갖고 더 현실적이고 냉철한 프레임워크를 만든다면 돈이 있는 투자자(그 도구를 사용하는 기술 회사나 학계 공동체)가 지속 가능성을 만드는 일을 도울 수 있을 겁니다. 이런 지속성 문제를 어떻게 해결할지 고민하는 것은 (예를 들어) 구글이 해야 할 일이 아니라 우리가 고민해서 그들에게 어떻게 우리를 도와야 하는지 이야기하고 그들이 우리와 함께 할 때 일을 해쳐나가는 것이 우리가 해야 할 일입니다. 그러나 지금 당장 중요한 프로젝트 대부분은 이런 접근이 어렵습니다. 그리고 어떤 경우도 간단하게 해결할 수 있는 문제가 아닙니다. 하지만 공유 자원이 이런 문제를 해결할 수 있는 접근법이 될 수 있습니다. 함께 생각해봅시다!

개인적으로 제가 참여했던 여러 오픈소스 프로젝트에서 어떤 노력을 했는지 다시 살펴보게 되어 흥미로웠습니다. 그 노력 중 하나도 지속적이지도, 지속되지도 않았습니다. 🙂 또한 그렇게 참여했던 프로젝트에서 Ostrom의 디자인 원칙에 맞지 않은 부분도 볼 수 있었습니다. 이 이야기는 다른 포스트에서 이어가도록 하겠습니다.

— 타이투스

특별히 Cameron Neylon과 Michael Nielsen에게 감사합니다. 이들은 Elinor Ostrom의 업적을 몇년 전에 알려줬습니다. Nadia Eghbal의 설명에도 감사를 전합니다. 주제에 대한 설명이 이 글의 재료가 되었고 앞으로도 같이 고민하며 전진했으면 합니다.

이 글을 쓰며 많은 이름을 언급했는데 Luiz Irber, Katy Huff, Katie Mack, Cory Doctorow, Jake VanderPlas, Tracy Teal, Fernando Perez, Michael Crusoe와 Matthew Turk에게 감사를 전합니다. #scifoo18과 #gccbosc에서도 같은 주제로 대화를 할 수 있었기에 SciFoo와 BOSC에도 감사합니다.

Buck Shlegeris의 My advice on studying algorithms를 번역했다.


알고리즘 학습에 대한 조언

소프트웨어공학 면접에서는 화이트보드 알고리즘 질문을 종종 냅니다. 이런 질문을 어떻게 공부해야 하는지 조언을 하려고 합니다. (저는 구글과 애플을 포함한 수많은 화이트보드 면접을 통과했습니다. 그리고 프로그래머가 이런 알고리즘 면접을 준비하도록 돕는 일이 제 직업의 일부입니다. 게다가 다양한 분야의 개발자를 대상으로 200회 이상의 기술 면접을 치뤘습니다.)

이 글은 Triplebyte이 아닌 제 자신으로서 쓰는 글입니다.

알고리즘 외에도 면접에 관한 여러 주제가 있습니다. 이런 주제는 Triplebyte의 포스트에서 잘 다루고 있습니다. 이 글에서 중요하게 다루려고 하는 내용은 Triplebyte의 포스트에서 2번 항목에 해당합니다.

배경: 왜 회사는 알고리즘 문제를 낼까요?

실생활에서 프로그래머가 이진트리 검색이나 그래프 탐색 알고리즘을 구현하는 시간은 거의 존재하지 않습니다. 그런데 왜 회사는 알고리즘에 대해 많은 질문을 낼까요?

이 질문을 존 왓슨과 코난 도일의 관점으로 해석할 수 있습니다. “회사가 알고리즘 문제를 내는 것이 왜 유용한가?”, 그리고 “어떤 회사가 알고리즘 질문을 하는 실제 일반적 원리가 무엇인가?”가 그 관점입니다.

먼저 알고리즘을 물어보는 이유를 설명하려고 합니다. 그리고 더 나아가 이 유행에 대한 냉소적인 입장에서 설명합니다.

먼저 직업 프로그래머 대다수가 아주 기초적인 일을 수행하지 못합니다. 예를 들어 고객 객체 목록이 있고 각 고객 객체에 구입 객체 배열이 존재합니다. 지난 주에 가장 많이 구입한 고객 다섯 명의 이름을 찾으려고 합니다. 제 예상에는 직업 프로그래머의 50%가 이 문제를 30분 이내에 풀지 못합니다. 이런 사람들을 실수로라도 채용하고 싶지 않을겁니다.

조금 덜 비관적으로 가정해봅시다. 프로그래밍 일을 위해 누군가 면접을 볼 때 어렵고 혼란스러운 문제를 잘 풀어낼 수 있는지 알아내려고 할겁니다. 모든 세세한 내용을 머리에 담고 있어야 풀 수 있는 것을 말이죠. 실생활에서 혼란스럽고 복잡한 문제가 존재하는 이유는 그 프로젝트를 몇 주 동안 봐야 할 만큼 큰 규모고 소프트웨어의 여러 부분을 동시에 고려해야 하기 때문입니다. 하지만 면접은 일반적으로 그렇게 깊은 프로그래밍 문제를 다룰 만큼 시간이 넉넉하지 않습니다. 그래서 규모가 크기 때문에 복잡한 문제를 물어보는 것보다 짧고 복잡한 질문을 물어보는 것입니다.

그럼 어떻게 복잡하지만 쉽게 설명할 수 있는 짧은 코딩 문제를 낼 수 있을까요? 그런 관점으로 생각해보면 제 생각에 여기서는 알고리즘이 좋은 선택입니다. 알고리즘은 컴퓨터 과학에서 대부분의 소프트웨어 엔지니어가 알고 있는 복잡한 분야며 쉽게 설명할 수 있고 구현하기 힘든 문제가 많이 존재합니다.

여기 조금 냉소적인 설명이 따라옵니다.

면접 프로세스는 이상할 정도로 끈적합니다. 엔지니어링팀은 팀의 기술 면접을 통과한 사람으로만 구성되어 있습니다. 그래서 모두가 면접이 옳은 방식이라고 믿게 되고 면접 과정이 소프트웨어 엔지니어링 능력을 측정하는데 매우 정확하다고 생각하게 됩니다. 그래서 회사에 알고리즘 면접 문화가 생기고 나면 그 이후로 바꾸기가 어려워집니다.

또한 모두가 알듯 구글은 10년 전에 놀라운 팀이 있었습니다. (지금은 그 당시보다 적습니다.) 그 당시에 구글은 알고리즘 면접 질문을 했습니다. 대부분의 회사가 자신들이 구글이 아니라는 점에 조금 불안했는지 (알다시피 최고의 지원자는 구글에 다 잃었으니까요), 이 회사도 구글 면접 과정을 따라하기 시작했습니다.

최악의 경우로 보면 알고리즘 면접은 기괴하고 못살게 구는 절차로 바뀌어 버립니다. 가끔 회사에서 무작위 난제를 내는 것이 위대한 지원자를 찾는 비밀 병기라고 단단히 착각해버려서 그런 생각을 바꾸는 일이 불가능한 경우가 있습니다.

종합해서 말하자면, 저는 이런 전통적이고 어려운 알고리즘 문제를 면접에서 안냈으면 합니다. 최악으로는 알고리즘 문제가 극단적으로 나쁜 면접 질문이 될겁니다. 제가 특히 싫어하는 질문은 이런 상황을 위해 따로 적어뒀습니다. 알고리즘 질문은 난제나 여러 통찰을 요구하는 경우에 특히 나쁜 질문입니다. (만약 알고리즘 면접 과정을 만들고 싶다면 언제든지 이메일을 보내시기 바랍니다. 이런 문제가 없는 질문을 하는 방법에 대해 더 자세한 의견을 드릴 수 있습니다.)

어떻게 공부하나요?

추가: Haseeb Qureshi의 블로그 포스트를 읽었는데 이 글에 동의합니다. 그리고 이 글이 좀 더 상세하다고 생각합니다. “일반 학습 전략”과 “프로그래밍 면접 공부 가이드” 부분을 읽으세요.

저는 알고리즘 문제에 답하려면 두 가지 다른 기술이 필요로 하다고 생각합니다. 첫째로 모든 대표적 알고리즘과 자료 구조 문제를 알아야 합니다. 둘째로는 부담되는 상황에서 알고리즘 논리를 화이트보드에 빠르게 풀어나갈 수 있어야 합니다. 이 두 주제를 나눠서 얘기해보려고 합니다.

표준 알고리즘 자료

회사의 시험을 준비하는데 있어 습득해야 하는 거의 필수적인 핵심 알고리즘 모음이 있습니다. 회사에서는 이런 목록에 들어있지 않은 질문은 하지 않으려고 합니다. 좋은 프로그래머 다수가 이 목록에 없는 질문에 대해서는 답을 모르기 때문이며 그래서 목록 외 질문을 냈다가 회사는 좋은 사람을 뽑는데 실패하게 됩니다.

알아야 할 자료구조는 다음과 같습니다.

  • list 구조: 배열, 동적 배열, 링크드 리스트(linked list)
  • set과 map 구조: 해시맵, 이진 검색 트리, 힙

여기서 언급한 자료구조는 필수 메소드가 어떻게 구현되어 있는지, 런타임은 어떻게 동작하는지 알아야 합니다. (list의 필수 메소드는 set, get, pushAtEnd, popAtEnd, insertByIndex, removeByIndex, set의 필수 메소드는 insert, remove, contains? 입니다.) 자료구조 구현을 어떻게 사용하는지 알아야 합니다. 예를 들면 getNearestElementTo(x) 메소드를 구현할 수 있어야 합니다. 이 메소드 즉, x와 가장 가까운 값을 찾는 구현을 하려면 이진 검색트리를 알아야 합니다.

이 문제를 해결하는데 이런 내용을 알아야 합니다.

  • 이진 검색트리 구현에 균형을 맞추는 코드가 필요하다는 점을 알아야 하지만 세부 내용은 몰라도 괜찮습니다. (선택 자료: 자기 균형 BST을 어떻게 구현하는지 빠르게 배우고 싶다면 이 트립을 참조하세요. 어떻게 레드블랙 트리가 동작하는지 이해하고 싶다면 좌편향 레드블랙 트리 또는 2-3-4 트리를 배우세요.)
  • 큐를 스택 두 개로 구현할 수 있다는 점을 알아야 합니다.

다음 알고리즘은 어떻게 구현하는지 알아야 합니다.

  • 그래프 알고리즘: 너비 우선 탐색(breadth first search), 깊이 우선 탐색(depth first search), 다익스트라 알고리즘 (dikstra’s algorithm)
  • 빠른 정렬 알고리즘 하나. 병합 정렬(mergesort) 또는 퀵 정렬(quicksort)
  • 배열에서 수행하는 이진 검색. 이 알고리즘은 제대로 작성하기 매우 까다롭고 대략적으로 알고리즘을 이해하고 있더라도 코드로 작성해볼 가치가 있습니다.

그리고 Big O 표기법도 대충이라도 편하게 사용할 수 있어야 합니다.

이 모든 내용을 어떻게 배워야 하나요? 제가 가장 좋아하는 자료는 Skiena의 Algorithm Design Manual입니다. 위에서 언급한 모든 내용을 챕터 26에서 다룹니다. 이 책을 좋아하는 이유는 저술 방식이 참여를 유도하고 각 부분에서 중요한 자료에 잘 초점을 맞추고 다루는데 이런 방식은 중요하다고 생각합니다. 이 책은 인터넷에서 무료로 찾을 수 있습니다. 이 책의 단점은 예제가 C로 작성되었다는 점인데 C를 읽지 못하는 개발자라면 접근성이 좋지 않습니다. 저는 챕터 16, 12는 꼭 읽어야 한다고 생각합니다. 이 부분은 인터뷰에서 나올 가능성이 극히 낮지만 필요 없다고 생각하는 부분이 진정 핵심적인 부분을 잘 보강한다고 생각하기 때문입니다.

이런 부분에 대략적인 설명을 보고 싶다면 Craking the Coding InterviewInterviewCake.com의 설명이 좋습니다.

저는 Skiena의 책이 극단적일 정도로 건조하고 딱딱한 유명 CLRS 교재보다 낫다고 생각합니다.

그래프 알고리즘에 대한 글을 쓴 적이 있는데 참고가 되었으면 좋겠습니다.

표준 알고리즘 기술

여기까지 인터뷰에 핵심적으로 필요한 부분을 확인했습니다. 이제 다른 종류의 프로그래밍 기술로 무엇을 테스트하는지 확인하고 제가 선호하는 학습 자료도 함께 확인합니다.

이런 기술에 있어서는 Cracking the Coding Interview(이하 CtCI) 책이 가장 유용합니다. 이 책에 대해서 작성한 글입니다.

알고리즘 면접 문제 중 가장 일반적이고 중점적으로 다뤄지는 요소는 다음과 같습니다.

  • 동적 프로그래밍: Skiena 책의 챕터 8 또는 CtCI에서 이 주제의 챕터에서 학습합니다.
  • 재귀: CtCI에 이 주제에 대한 멋진 챕터가 있습니다.
  • 유명 자료 구조를 반복(iterating)하는 문제: CtCI에서 각각의 자료 구조를 다룰 때 이 문제도 함께 다룹니다. 예를 들어 BST에서는 CtCI 트리 챕터를 참고할 수 있습니다.
  • 문제 해결을 위해 빠른 자료 구조를 조합하기: 이런 문제에 대한 예제는 이 글에서 확인할 수 있습니다.

CtCI에서 살펴볼 수 있는 많은 문제를 살펴보는 방법이 제가 드리는 가장 주요한 조언입니다. 이 문제에서 가장 중요하다고 생각하는 부분은 위 목록과 같습니다.

이런 부류의 문제를 어떻게 학습하는지에 대해 일반적인 생각은 이렇습니다. 제 생각엔 답안을 “훔쳐보는” 일은 그래도 괜찮다고 생각합니다. 면접 문제 푸는 일을 내던지고 아예 포기하는 것보다는 문제 풀다가 막히면 해결책을 보는 방법이 차라리 나은 접근이기 때문입니다.

알고리즘 면접에서 성공하기 위한 비기술적 측면

이런 질문은 실제로 부담되는 환경에서 답하는 연습을 해야 합니다. 진짜 사람이 질문하는 상황에서 말이죠. 이 부분에 대해서는 Triplebyte의 블로그 포스트에서 다루고 있고 2, 3, 7번을 읽어보기 바랍니다.

알고리즘과 자료구조에 대해 더 배우기

취업 목적 학습을 넘어서 본인을 위해 즐겁게 알고리즘과 자료구조를 배우고 싶다고 가정해봅시다. 어떻게 더 배워야 할까요?

가장 쉬운 방법은 위에서 필수로 배워야 한다고 한 핵심 자료 구조에 포함되지 않는 자료 구조 중 상대적으로 간단한 자료 구조를 학습하는 방법입니다. 트립, 스킵 리스트, 증강 이진검색트리, 서로소 집합 자료구조가 그 예로 모두 쉽게 이해할 수 있는 편이며 모두 멋진 알고리즘입니다.

자료구조 주제 중 이해하기 어렵지만 노력해서 이해하면 좋은 주제도 있습니다. 예를 들어 이 슬라이드에서는 이진트리와 2-3-4 트리를 설명합니다.

흥미로운 자료 구조를 배울 수 있는, 제가 좋아하는 자료는 다음과 같습니다.

  • Skiena의 챕터 12와 이후 챕터
  • 스탠포드의 멋진 강의인 CS166. 이 강의의 슬라이드는 멋지고 읽기 좋은 편입니다. 저는 여기서 다룬 문제가 즐거웠습니다. 자료구조와 더 놀고 싶다면 이 프로젝트 아이디어 핸드아웃을 추천합니다.
  • 저는 이런 작업처럼 그다지 어렵지 않은 자료 구조 문제를 아마추어 활동으로 재미삼아 한다는 점이 자랑스럽습니다. 이 문제를 풀기 위한 해결책으로 고급 자료구조에서 얻은 몇 아이디어를 적용했다는 점이 멋지지 않나 생각합니다.

자료

웹사이트 설정

웹페이지 색상을 선택하세요

Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.