• 프론트엔드 기술 조감도 : Babel

    자바스크립트 컴파일러 Babel과 관련된 플러그인, 프리셋, 폴리필 등의 개념에 대해 소개합니다.

    들어가며

    오늘날 프론트엔드 생태계에서 Babel은 필수 도구라 부르기에 부족함이 없다. 그런 만큼 튜토리얼과 스타터 킷 등 쉽게 도입할 수 있는 수단도 잘 마련되어 있다. 아이러니하게도, 그런 탓인지 Babel을 쓰면서도 막상 각 구성요소의 역할을 정확히 이해하지 못하는 경우를 종종 본다.

    이 글은 Babel과 관련된 큰 단위의 개념에 대해 소개한다. 설정 옵션이나 주의할 점 등, 각 항목별로 알아야 할 자세한 내용은 공식 문서가 이미 충분히 잘 설명하고 있어 굳이 다루지 않았다.

    NOTE: 이 글은 Babel v7.1.0 기준으로 작성되었습니다.

    Babel?

    Babel은 최신 스펙 및 프로포절 등을 포함하는 자바스크립트 코드를 ES5 환경에서 잘 동작하도록 컴파일하는 자바스크립트 컴파일러다.

    Babel의 전신은 6to5 라는 프로젝트다. 이름에서 드러나듯, 당시 최신 ECMAScript 스펙이던 ECMAScript2015(ES6) 코드를 ES5로 컴파일하는 도구였던 6to5가 2015년,50만 다운로드를 앞두고 작성한 블로그 글에서 발표한 새 이름이 바로 Babel이다.

    6to5 is now Babel.

    Babel 공식 웹사이트의 What is Babel? 문서는 아래와 같이 시작한다.

    바벨은 주로 ECMAScript 2015+ 코드를 현재 및 과거의 브라우저와 같은 환경에서 호환되는 버전으로 변환하는데 주로 사용되는 도구입니다. Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments.

    좀 더 쉽게 말하자면, Babel은 최신 자바스크립트 스펙과 기술을 사용해 작성한 코드를 오래 된 브라우저에서도 동작하는 코드로 변환해준다. 그렇다면 Babel은 과연 어떤 코드를 어떻게 변환할지 어떻게 판단할까?

    참고자료

    플러그인

    Babel 플러그인은 Babel이 어떤 코드를 어떻게 변환 할지에 대한 규칙을 나타낸다.

    Babel이 소스 코드를 ES5 코드(타겟 코드)로 변환하는 과정은 크게 다음 세 단계로 나누어 생각할 수 있다.

    1. 구문 분석parsing: 소스 코드를 읽어 추상 구문 트리로 변환하는 단계.
    2. 변환transformation: 정해진 규칙대로 추상 구문 트리에 변경을 가하는 단계.
    3. 출력printing: 변경이 가해진 추상 구문 트리를 다시 코드로 출력하는 단계.

    이 과정을 의사 코드로 표현하면 다음과 같다.

    import { AST, Rule } from "./model";
    import rules from "./rules";
    
    function parse(sourceCode: string): AST {
      /* 소스 코드를 AST로 변환 */
      /* ... */
      return sourceAST;
    }
    
    function transform(sourceAST: SourceAST, rules: Rule[]): AST {
      /* rules 에 들어있는 규칙에 기반해 AST 수준에서 코드 변환 */
      /* ... */
      return transformedAST;
    }
    
    function print(transformedAST: AST): string {
      /* 변환된 AST를 다시 코드로 변환 */
      /* ... */
      return outputCode;
    }
    
    function babel(sourceCode: string, rules: Rule[]) {
      return print(transform(parse(sourceCode), rules));
    }
    

    아무런 설정 없이 실행한다면 Babel은 위 코드에서 rules 변수가 [] 인 상황처럼 동작한다. 즉, 아무런 변경이 가해지지 않은 소스 코드를 그대로의 결과물을 내뱉는다. Babel이 실제로 쓸모를 갖기 위해선 transform 단계에서 사용할 코드 변환 규칙(Rule)이 추가되어야 한다.

    Babel 플러그인은 바로 이 규칙의 역할을 한다. 모든 플러그인은 어떤 조건을 만족하는 코드를 만나면 그 코드를 이렇게 해석하고 변환하라는 규칙을 담고 있다. 몇 가지 예시 문서를 보면 플러그인의 역할이 무엇인지 쉽게 감을 잡을 수 있을 것이다.

    플러그인의 종류

    Babel 플러그인은 크게 두 종류로 나뉜다.

    • 변환 플러그인은 Babel에게 코드에 특정 표현/문법이 나타나면 특정 방식으로 변환하도록 한다.
    • 문법 플러그인은 Babel이 코드의 특정 표현/문법을 이해(파싱)할 수 있게 한다. 어떤 문법과 연관된 변환 플러그인은 문법 플러그인을 자동으로 포함한다.

    어떤 플러그인이 어느 경우에 해당하는지는 플러그인 이름의 prefix (plugin-transform-... 또는 plugin-syntax-...) 유추할 수 있는 경우가 많다.

    참고자료

    프리셋

    Babel 프리셋은 Babel 플러그인의 모음이다.

    함께 자주 쓰이는 (쓰여야 하는) 특정 플러그인의 조합을 생각해보자. 매번 프로그래머가 플러그인을 일일이 더해주는 일은 번거롭고 쉽게 실수를 유발할 것이다. 때문에 Babel은 프리셋이라는 개념을 통해 여러 플러그인을 묶어서 다루는 방법을 제공한다.

    Babel 팀이 공식적으로 지원하는 프리셋으로는 preset-typescript, preset-minify, preset-react 등이 존재하며, 프리셋은 기본적으로 플러그인의 조합에 불과한 만큼 그 외에도 수많은 커뮤니티 기반 프리셋이 존재한다.

    preset-env

    여러 플러그인의 정적인 집합인 여타 프리셋과 달리, preset-env는 필요한 플러그인을 프로젝트가 지원하고자 하는 환경에 기반해 빌드 타임에 동적으로 결정하는 ✨특별한✨ 프리셋이다.

    preset-env를 사용하는 프로그래머는 지원하고자 하는 환경을 browserslist 설정 형식으로 명시할 수 있다. preset-env는 이렇게 명시된 환경에 필요한 플러그인을 compat-table의 정보를 활용해 결정하고, 빌드 과정에 포함시킨다.

    참고자료

    폴리필

    Babel 폴리필은 최신 ECMAScript 환경을 만들기 위해 코드가 실행되는 환경에 존재하지 않는 빌트인, 메소드 등을 추가한다.

    Babel을 사용해 최신 ECMAScript 명세에 도입된 문법을 ES5까지의 문법으로 컴파일할 수 있다. 하지만, 새 스펙에 추가된 빌트인이나 프로토타입 메소드 등의 추가는 Babel의 역할이 아니다.

    빌드 과정에서 Babel을 거쳤더라도 (ES5 이후 환경에 추가된) Promise와 같은 빌트인 또는 Array.prototype.includes 등의 인스턴스 메소드가 코드에 남아있을 수 있다. 당연하지만, 해당 빌트인/메소드를 지원하지 않는 환경에서는 에러가 발생한다.

    Babel 폴리필은 실행 환경에서 이런 빌트인, 메소드 등이 존재하는지 확인하고, 만약 존재하지 않는다면 추가한다. 플러그인과 프리셋은 빌드 타임 동작에 대한 설정인 반면, 폴리필은 런타임에 동작한다는 점에 유의하라.

    Babel 폴리필은 내부적으로 core-js에 의존한다.

    preset-env와의 사용

    Babel 폴리필 전체를 빌드에 포함한다면 번들 사이즈가 너무 커질 수 있다. preset-envuseBuiltIns 옵션을 사용하면 빌드 타임에 babel-polyfill 임포트를 꼭 필요한 폴리필 임포트로 대체해 번들이 필요 이상으로 커지는 일을 방지할 수 있다. 자세한 설정 방법은 공식 문서를 참고하라.

    참고자료

    요약

    • Babel은 최신 자바스크립트 문법을 ES5 표준으로 변환해주는 컴파일러다.
    • 플러그인을 통해 Babel에 컴파일 규칙을 추가할 수 있다.
    • 프리셋을 사용해 여러 서로 다른 플러그인을 한 묶음으로 다룰 수 있다.
      • preset-env를 사용해 지원하고자 하는 환경에 필요한 플러그인을 똑똑하게 명시할 수 있다.
    • 폴리필을 사용해 런타임에 필요한 빌트인, 프로토타입 메소드를 추가할 수 있다.
      • preset-env와 함께 사용하면 꼭 필요한 폴리필만 불러올 수 있고, 따라서 번들이 필요 이상으로 커지는 일을 방지할 수 있다.

    ~ 🌝 끝 🌚 ~

  • 잘 알려지지 않은 유용한 CSS 속성들

    pointer-events, object-fit, will-change 등 상대적으로 덜 알려졌지만 알아두면 언젠가 유용하게 써먹을 CSS 속성을 소개합니다.

    들어가며

    웹 개발자로 일하다 보면 CSS 의 세계는 참 무궁무진하다는 사실을 자주 느낀다. 매일같이 다루는 몇 가지 속성만으로도 대부분의 상황을 충분히 커버할 수 있지만, 오히려 그래서인지 여러 흔치 않은 용례를 커버하는 속성을 맞닥뜨릴때면 더더욱 즐겁게 놀라게 된다.

    이 글에서는 잘 알려지지 않은, 하지만 알아두면 분명 유용하게 써먹을 일이 생길 CSS 속성 몇 가지를 소개하려 한다. 하나씩 찾아보면 이미 잘 정리된 자료들이 많은 만큼, 해당 속성의 모든 내용을 세세히 다루기보단 이런 속성이 존재하며 대략 어떤 용도로 사용된다는 점 정도를 소개하는 데에 초점을 맞추었다.

    pointer-events: 클릭 이벤트 허용 여부

    pointer-event 속성을 통해 엘리먼트가 마우스 이벤트(호버, 클릭, 드래그 등)에 어떻게 반응할지를 지정할 수 있다. 대부분의 속성 값은 SVG 전용이므로, pointer-events: none을 설정하여 마우스 이벤트의 타겟이 될 수 없도록 할 수 있다는 점만 기억하자.

    해당 속성이 지정되었더라도 반드시 마우스 이벤트의 이벤트 리스너가 호출되지 않을 거라 보장되지 않는다는 점은 주의해야 한다. 예를 들어, 부모 엘리먼트가 pointer-events: none 속성을 갖고 있어도 자식 중pointer-events: auto를 가진 엘리먼트가 있다면, 해당 자식 엘리먼트에 트리거 된 이벤트가 버블링 또는 캡쳐링 되는 과정에서 부모 엘리먼트의 이벤트 리스너가 호출될 수 있다.

    <ul>
      <li><a href="https://developer.mozilla.org">MDN</a></li>
      <li>
        <a href="http://example.com" style="pointer-events: none;">example.com</a>
      </li>
    </ul>
    

    가능한 속성 값

    pointer-events: auto;
    pointer-events: none;
    /* 이하 SVG 전용 값 생략 */
    

    touch-action: 브라우저에게 맡길 터치 액션 지정

    기본적으로 터치 이벤트의 처리는 브라우저가 담당하는 영역이다. touch-action 속성을 통해 어떤 요소 내에서 브라우저가 처리할 터치 액션의 목록을 지정할 수 있다. 표준 터치 제스쳐로는 터치를 사용한 스크롤(panning)과 여러 손가락을 사용한 확대/축소(pinch zoom)이 존재하며, 브라우저에 따라 더블 탭으로 확대 등 표준이 아닌 여러 제스쳐를 지원하는 경우도 있다.

    touch-action 속성의 값으로 auto 이외의 값을 줄 경우, 해당 속성에 명시해준 터치 액션만이 브라우저에 의해 처리된다. 예를 들어, touch-action: pinch-zoom 속성을 갖는 엘리먼트에서는 터치를 사용한 스크롤이 (자바스크립트로 별도로 처리를 해 주지 않는 이상) 무시된다.

    가능한 속성 값

    touch-action: auto; /* 기본 값 */
    touch-action: none; /* 브라우저가 모든 터치 이벤트를 무시하도록 설정 */
    
    touch-action: pan-x; /* 특정 축으로의 터치를 사용한 스크롤 허용 */
    touch-action: pan-y;
    
    touch-action: pan-left; /* 특정 방향으로의 터치를 사용한 스크롤 허용 */
    touch-action: pan-right;
    touch-action: pan-up;
    touch-action: pan-down;
    
    touch-action: pinch-zoom; /* 핀치 줌(여러 손가락을 사용한 확대/축소) 허용 */
    
    touch-action: manipulation; /* 터치를 사용한 스크롤, 핀치 줌만 허용하고 그 외 비표준 동작 (더블 탭으로 확대 등) 불허용 */
    
    touch-action: pan-y pinch-zoom; /* 동시에 여러 값 지정 가능 */
    

    user-select: 선택 상호작용

    user-select 속성을 사용해 엘리먼트 내부에서 텍스트 선택이 일어났을 때의 동작을 설정할 수 있다. 기본 동작 이외에 선택이 불가능하게 지정할 수도 있고, 엘리먼트 내에서 선택이 일어나면 무조건 엘리먼트 전체가 선택되는 식의 동작도 설정 가능하다.

    <div
      style="user-select: auto; border: 1px solid black; padding: 12px; margin: 12px;"
    >
      user-select: auto;
    </div>
    <div
      style="user-select: none; border: 1px solid black; padding: 12px; margin: 12px;"
    >
      user-select: none;
    </div>
    <div
      style="user-select: all; border: 1px solid black; padding: 12px; margin: 12px;"
    >
      all <span style="border: 1px solid black;">child</span>
    </div>
    <div
      style="user-select: text; border: 1px solid black; padding: 12px; margin: 12px;"
    >
      user-select: text
    </div>
    
    user-select: auto;
    user-select: none;
    all child
    user-select: text

    가능한 속성 값

    user-select: auto; /* 기본값 (::after, ::before 는 선택되지 않고, 부모의 속성을 따름) */
    user-select: text; /* 선택 가능 */
    user-select: none; /* 선택 불가능 */
    user-select: all; /* 엘리먼트 내에서 선택이 일어나면 해당 엘리먼트 전체가 선택된다 */
    

    object-fit: 대체되는 엘리먼트의 내용물과 컨테이너 사이 관계 지정

    img, video 등과 같이, 내용물이 HTML 문서의 바깥에 존재하는 엘리먼트를 대체되는 엘리먼트라 부른다. 이 때, 외부에 존재하는 내용물의 크기가 컨테이너의 그것과 차이날 때, 화면에는 어떻게 나타나야 할지 지정할 필요가 생긴다. 예를 들어, 너비 150px, 높이 200px 짜리 img 엘리먼트의 src로 너비 50px, 높이 600px 의 이미지가 지정되었다면, 이 이미지는 어떻게 보여야 할까?

    이런 상황에서 대체되는 엘리먼트의 내용물이 컨테이너를 어떻게 채울지를 지정하는 데에 사용되는 것이 object-fit 속성이다. 이 속성의 동작방식을 설명하는 데에는 말보다 MDN 페이지의 CSS 데모가 훨씬 효과적일 것이다. 링크를 타고 가기 귀찮은 독자를 위해 MDN 의 예제도 아래에 (약간의 설명과 함께) 옮겨두었다.

    가능한 속성 값과 예시

    fill

    내용물의 가로세로비를 무시하고 컨테이너의 크기에 맞추어 늘리거나 줄인다. 원래 비율이 유지되지 않으므로, 컨테이너의 크기에 따라 내용물이 가로 혹은 세로로 늘어날 수 있다.

    <div>
      <img
        style="width: 150px; height: 100px; border: 1px solid #000; object-fit: fill;"
        src="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
        alt="MDN Logo"
      />
      <img
        style="width: 100px; height: 150px; border: 1px solid #000; margin-top: 10px; object-fit: fill;"
        src="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
        alt="MDN Logo"
      />
    </div>
    
    MDN Logo MDN Logo

    contain

    내용물의 가로세로비를 유지하는 채로, 내용물이 컨테이너에 포함되는 최대 크기가 되도록 늘리거나 줄인다.

    <div>
      <img
        style="width: 150px; height: 100px; border: 1px solid #000; object-fit: contain;"
        src="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
        alt="MDN Logo"
      />
      <img
        style="width: 100px; height: 150px; border: 1px solid #000; margin-top: 10px; object-fit: contain;"
        src="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
        alt="MDN Logo"
      />
    </div>
    
    MDN Logo MDN Logo

    cover

    내용물의 가로세로비를 유지하는 채로, 내용물이 컨테이너 전체를 덮는 최소 크기가 되도록 늘리거나 줄인다.

    <div>
      <img
        style="width: 150px; height: 100px; border: 1px solid #000; object-fit: cover;"
        src="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
        alt="MDN Logo"
      />
      <img
        style="width: 100px; height: 150px; border: 1px solid #000; margin-top: 10px; object-fit: cover;"
        src="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
        alt="MDN Logo"
      />
    </div>
    
    MDN Logo MDN Logo

    none

    내용물이 전혀 리사이징 되지 않는다.

    <div>
      <img
        style="width: 150px; height: 100px; border: 1px solid #000; object-fit: none;"
        src="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
        alt="MDN Logo"
      />
      <img
        style="width: 100px; height: 150px; border: 1px solid #000; margin-top: 10px; object-fit: none;"
        src="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
        alt="MDN Logo"
      />
    </div>
    
    MDN Logo MDN Logo

    scale-down

    nonecontain 중 내용물의 크기가 더 적은 쪽과 동일하게 동작한다.

    <div>
      <img
        style="width: 150px; height: 100px; border: 1px solid #000; object-fit: scale-down;"
        src="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
        alt="MDN Logo"
      />
      <img
        style="width: 100px; height: 150px; border: 1px solid #000; margin-top: 10px; object-fit: scale-down;"
        src="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
        alt="MDN Logo"
      />
    </div>
    

    overflow-wrap: 오버플로우가 일어날 때 단어 내 줄바꿈 처리

    CJK 언어로 된 사이트를 자주 만드는 개발자라면 word-break: keep-all; 속성을 아주 유용하게 사용하고 있을 것이다. keep-all 속성값을 사용하면 CJK 에서도 단어 단위로 줄바꿈을 끊어줄 수 있다. 그런데 만약 한 단어의 길이가 컨테이너의 너비보다 더 긴 경우에는 어떤 일이 생길까?

    <div class="example-container">
      <div style="width: 200px; border: 1px solid black; word-break: keep-all;">
        굉장히길고엄청나게길면서굉장히길고엄청나게길면서굉장히길고엄청나게길면서굉장히길고엄청나게길면서의미는없는문자열
      </div>
    </div>
    
    굉장히길고엄청나게길면서굉장히길고엄청나게길면서굉장히길고엄청나게길면서굉장히길고엄청나게길면서의미는없는문자열

    위에서 보이듯, 단어 단위로 끊기로 설정할 경우 단어의 길이에 따라 오버플로우가 컨테이너를 넘쳐버릴 수 있다. 이런 경우 overflow-wrap 속성에 break-word 값을 주어서 오버플로우가 일어나는 경우 적절하게 단어 사이에서 줄바꿈이 일어나도록 설정할 수 있다.

    <div class="example-container">
      <div
        style="width: 200px; border: 1px solid black; word-break: keep-all; overflow-wrap: break-word;"
      >
        굉장히길고엄청나게길면서굉장히길고엄청나게길면서굉장히길고엄청나게길면서굉장히길고엄청나게길면서의미는없는문자열
      </div>
    </div>
    
    굉장히길고엄청나게길면서굉장히길고엄청나게길면서굉장히길고엄청나게길면서굉장히길고엄청나게길면서의미는없는문자열

    가능한 속성 값

    overflow-wrap: normal; /* 기본 */
    overflow-wrap: break-word; /* 오버플로우가 일어나면 단어를 쪼개서 줄바꿈 */
    

    list-style-position: 리스트 마커 위치 지정

    리스트 아이템 앞에 따라오는 리스트 마커는 기본적으로 li 태그 바깥에 위치한다.

    <ul style="padding-left: 20px;">
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>
    
    • 1
    • 2
    • 3

    list-style-position 속성의 값으로 inside를 줘서 마커가 li 태그 안로 들어오도록 설정할 수 있다.

    <ul style="padding-left: 20px; list-style-position: inside;">
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>
    
    • 1
    • 2
    • 3

    가능한 속성 값

    list-style-position: inside; /* 마커가 `li` 태그 안에 위치 */
    list-style-position: outside; /* 마커가 `li` 태그 바깥에 위치 */
    

    will-change: 값이 변경될 속성에 대한 힌트

    웹이 정적 문서를 위한 플랫폼에서 동적으로 상호작용하는 복잡한 어플리케이션을 위한 플랫폼으로 진화함에 따라, opacity, transform 등의 CSS 속성 값이 동적으로 변화는 상황이 갈수록 자주 생긴다.

    이 때, will-change 속성을 사용해 브라우저에게 엘리먼트의 어떤 속성이 높은 확률로 변할 것인지 힌트를 줄 수 있다. 브라우저는 이 힌트를 사용해 앞으로 일어날 변화에 미리 대비해 더 매끄러운 트랜지션을 구사할 수 있다. 예를 들어, 다음 스타일시트는 .sidebar 엘리먼트의 transform 속성 값이 변할 것임을 나타낸다.

    .sidebar {
      will-change: transform;
    }
    

    가능한 속성 값

    will-change: auto; /* 기본값 */
    will-change: scroll-position; /* 엘리먼트의 스크롤 위치가 바뀔 것 */
    will-change: contents; /* 엘리먼트의 컨텐츠 중 일부가 바뀔 것 */
    
    /* 혹은 특정 CSS 속성을 명시할 수 있다. */
    /* transform, opacity, top, left, right, bottom 정도가 자주 사용된다. */
    will-change: transform;
    will-change: left, top; /* 여러 속성을 동시에 명시할 수도 있다. */
    

    당연하지만, 이런 준비 작업에는 비용이 든다. 필요하지 않은 상황에서도 will-change 속성을 너무 남발한다면 오히려 성능 저하가 일어날 수 있음을 유의하라. 기본적으로 CSS 속성 값 변경이 성능 문제 없이 잘 동작할 때는 will-change 를 직접 건드리지 않는 것이 좋다.

    참고 자료

    맺으며

    이상 몇 가지 (상대적으로) 덜 유명한 CSS 속성에 대해 알아보았다. 확실히 다른 속성에 비해 자주 사용될 만한 속성은 아니지만, 이 글에서 소개한 지식이 언젠가 도움이 되는 일이 있으리라 생각하고, 그러길 바란다. 생각보다 글이 길어져, 원래 처음 글을 쓰기로 마음먹은 이유인 CSS 카운터에 대해서는 나중에 다른 글에서 다루어보려 한다.

  • 모래 언덕을 오르는 일

    몽골 여행에서 가장 또렷하게 남은 한 기억에 관한 기록.

    작년 5월에 연휴를 끼고 열흘 남짓 몽골에 다녀왔다. 친구들과, 친구들의 친구들과, 친구의 친구의 친구까지 모인 열둘이 좁고 덜컹거리는 러시아산 6인용 밴 두 대에 나눠 타고 돌아다녔다. 지도엔 나오지 않는 길을 따라 안내해 준 가이드분 덕에 짧은 시간이지만 낯선 나라의 여러 모습을 볼 수 있었다.

    멀리서 바라본 사구의 모습

    신선하고 다채로운 경험이 많았다. 낮엔 햇빛이, 밤엔 냉기가 피부를 찌르는 고비 사막 앞 게르에서 묶은 바로 다음 날 오후, 앞이 안 보일 정도의 눈보라 속을 지나쳤던 해프닝 정도면 대충 짐작이 갈지 모르겠다. 그중에서도 유독 고비 사막의 한 사구(모래 언덕)를 기어오른 일이 – 실제로 걸린 시간은 한두 시간 남짓이었음에도 – 지금까지도 이따금 떠오른다.

    멀리서 바라본 사구는 생각보다 아담해 보였다. 고개를 조금 들어 바라보니 저 위에 개미처럼 보이는 사람들과 함께 정상이 보였다. 어차피 아래에서만 봐도 모래밖에 없는 게 뻔한 이 언덕을 왜 굳이 올라야 하나– 고개 들던 생각은 이내 얼른 올라갔다 와야지–하는 타협으로 바뀌었다. 막상 오르기 시작하니 나름 신기하고 신이 나기도 했다.

    고비 사막의 사구

    중턱쯤 되었을까, 가볍던 마음은 깡그리 사라졌다. 급해진 경사는 차치하더라도, 단단히 다져지지 않은 바닥이라는 복병이 너무 강력했다. 내딛는 발마다 발목 위까지 깊숙이 모래 속으로 빠졌다. 바닥이 붙든 발을 떼어 낼 때면 모래 주머니를 매단 다리를 들어올리듯 무거웠다. 오른 발을 들기 위해 힘을 주어 디딘 만큼 왼발은 그만큼 더 깊숙이 빠지곤 했다.

    더구나 아래에선 먼지가 날린다– 정도의 느낌이던 모래바람도 비교가 안 되게 거세졌다. 온몸을 감싸 매고 외투 모자를 덮어쓰고 스카프로 얼굴을 가린 것으로는 부족했다. 뺨이 따갑고 눈과 코가 매웠다. 어차피 앞도 안 보이겠다, 조금이라도 바람을 덜 맞으려 언젠가부터 걷는 대신 네발로 기기 시작했다. 기는 모습을 보고 웃던 친구들도 이내 비슷한 전략을 택했다. 몇 명은 중간에 더는 못 오르겠다 선언하고 먼저 내려갔다.

    다리가 아프고 목이 말랐다. 참고 다음 발을 내디디고, 참고 눈을 비비고, 참다 걸터앉아 얼굴을 가리고 숨을 고르기를 몇 번을 반복했는지 모르겠다. 얼마나 더 가야 돼? 도대체 얼마나 더 가야 하는 거야! 도대체… 하며 다음 손을 내디뎠는데, 익숙한 모래가 반겨줘야 할 자리에 아무것도 없었다. 닻을 내리려 내리꽂던 손은 몸보다 살짝 더 아래까지 가서야 멈췄다. 고개를 들어보니 지긋지긋한 모래 언덕이 아닌, 올라온 쪽과 비슷한 듯 다른 모습의 언덕 너머가 보였다. 옆을 보니 조금 먼저 올라와 앉아 쉬고 있는 친구들이 그제야 보였다.

    정상이었다. 우리는 정상에 올라있었다.

    정상에 오르기 조금 전 우리

    지칠 때면 그때의 모래 언덕을 떠올리며 그런 생각을 한다. 세상에는 모래 바닥이 놔주지 않는 무거운 발을 끌고, 피부에 모래바람을 맞으며 또 눈코입귀로 모래를 먹어가며 올라야만 볼 수 있는 풍경이 있는 것이라고. 포기하지만 않고 오르다 보면 그때처럼 나도 모르는 새 정상에 올라 있는 우리를 보게 될 것이라고. 그리고 그 언덕을 끝까지 올랐듯, 이번에도 결국 정상에 올라 그 너머를 볼 수 있으리라고.


    함께 여행을 다녀온 태현, 원재가 찍은 사진을 사용했습니다. (글에 나온 순서대로 태현 – 원재 – 태현의 사진)

  • 마지막 그 아쉬움은 기나긴 시간 속에 묻어둔 채

    안녕은 영원한 헤어짐은 아니겠지요

    지난 금요일, 그러니까 그제는 회사에서 가장 친한 동료 중 한 명의 마지막 출근날이었다. 회사의 누구도 원하지 않은 퇴사였지만 동료의 개인적인 상황과 약간의 불운, 부주의 등이 복잡하게 엮여 입사 1주년을 조금 앞두고 떠날 수 밖에 없는 상황이 되었다. 나는 조금 슬프고 많이 아쉽다. 아니 많이 슬픈지도 모르겠다. 아니 사실은 많이 슬프다.

    마지막 밤은 회사에서 우연히 곱창을 함께 먹으러 모였다가 지난 몇 달 부쩍 친해진 – 이제 ‘회사 동료’보다 ‘친구‘라는 단어가 더 잘 어울리는 – 사람들과 함께 보냈다. 술은 입에 대는 둥 마는 둥, 한 주의 피로가 모여 졸린 눈을 비비면서도 함께 시답잖은 이야기들로 새벽을 붙들다 결국 세 시 반이 조금 넘어 ‘앞으로도 자주 보자’는 뻔한 안녕과 함께 헤어졌다. 어디 외국으로 영영 나가는 것도 아닌데 자주 봐야지!

    어쩌다보니 이 년이 채 안 된 짧은 경력으로 벌써 세 번째 회사에 다니고 있다. 늘 함께 일했던 이들을 남겨두고 먼저 떠나는 입장이었다. 그래서인지 이렇게 친하고 든든한, 일상의 많은 부분을 함께한 동료를 떠나보내는 일이 좀 낯설다. 전 동료들도 이렇게 복잡한 기분이었을까– 하는 생각을 많이 했다. 어떻게 해야 잘 보내는걸까 고민하는 한편, 새로운 환경에 대한 기대로만 가득찼던 내 모습이 이제야 뒤늦게 좀 부끄럽기도, 미안하기도.

    첨부한 영상은 내 대학 시절의 큰 부분을 차지한 ‘여섯줄’이란 동아리에 들어가서 처음으로 참여했던 2013년도 봄학기 엔딩이다. (나는 영상 왼쪽 어딘가에 있다) 공연의 마지막 곡으로 부르는 합창을 우리는 엔딩이라 불렀다. 처음이어서 더 기억에 남는 건지, 그냥 노래가 좋은 탓인지, 누군가와 안녕을 이야기 할 때면 나는 이 노래가 생각난다. 이제는 사실 이 때 함께한 사람들 중에서도 자주 얼굴 보고 사는 사이는 거의 없지만…

    그래 뻔한 안녕이 영원한 헤어짐은 아니겠지요. 정말 앞으로도 얼굴 보면서 지냈으면 좋겠어요. 어딜 가서도 몸 건강히 잘 지내고, 다음에 볼 땐 그동안 잔뜩 쌓아둔 설레고 신나는 이야기를 풀어주길 바라요.

  • 블로그 재작성 작업기

    개인 블로그를 새롭게 만든 이유와 그 과정, 만들며 느낀 점을 적어보았습니다.

    지난 한 주에 걸쳐 블로그를 새로 만들었다. 오랜만에 글을 쓰는데, 부담도 적은 주제이고 새 블로그의 첫 글로 잘 어울릴 것 같아 작업기를 간단히 남겨본다.

    재작성의 이유

    먼저, 이미 잘 돌아가고 있던 블로그를 새로 만들기로 결정한 이유를 적어본다.

    기존 블로그가 가진 가장 큰 문제는 글을 써서 배포하는 과정에서의 비효율성이었다. 당시 (정확히 기억나지 않는 이유로) 사이트는 GitHub Pages를 사용해 빌드하고 글은 S3에 올린 것을 동적으로 받아오는 방식을 취했다. 덕분에 글을 한 번 써서 배포할때마다 다음 과정을 밟아야했다.

    1. 글을 작성해서 이미지와 함께 S3에 올린다
    2. 글 목록과 메타정보를 담은 JSON 파일을 업데이트한 뒤 S3에 올린다
    3. RSS 피드 파일을 업데이트한 뒤 S3에 올린다
    4. Nuxt.js를 한 번 빌드한 뒤 GitHub에 푸시한다

    특히 소스코드 수정이 전혀 없는데도 Nuxt.js를 정적 사이트 생성기로 사용했기 때문에 필요했던 4번 스텝이 할 때마다 매우 짜증났다. 추가적으로 글이 GitHub 에서 호스팅되지 않고, GitHub Pages의 캐시를 컨트롤할 수단이 없기 때문에 캐싱된 글의 수정 내역을 반영하기 위해선 쓸모없는 빌드를 다시 하는 등의 작업을 해야 하는 것도 귀찮았다.

    이런 귀찮은 과정을 겪다보니, 글을 써서 배포하는 과정이 불편한 것이 글을 쓰기 싫게 만들고, 글을 자주 쓰지 않으니 불편함을 매번 ‘이번엔 그냥 참고 하자’ 고 참고 넘어가는 악순환이 생겼다. 웹으로 편하게 작성, 수정, 미리보기가 가능한 UI의 소중함을 깨달았다. 물론 이 문제들은

    • S3에 올라간 글을 GitHub Pages로 옮겨오고
    • 배포를 위한 스크립트를 작성하고
    • CI를 셋업하면

    많은 부분을 해결할 수 있다. 그럼 왜 그렇게 하지 않고 굳이 새로 만들었느냐?

    가장 큰 이유는 기존 블로그의 기술 스택이 그다지 마음에 들지 않았기 때문이다. 기존 블로그는 Vue 기반의 Nuxt.js로 만들어졌다. 그런데 이 글 에서 밝혔듯 나는 둘 다 프로덕션에서 사용해보면 볼수록 React가 더 나은 기술이라는 나름의 확신이 생겼다. 발전 속도 자체도 차이가 나는 시점에서 Vue 기반 블로그를 유지할 필요가 없다고 느껴졌다.

    추가적으로, 이미 시간이 없어 미루어뒀지만 몇 달 간 쌓아둔 블로그 개선점이 몇 개 있었다. 꽤나 간단한 사이트인만큼, 해당 작업을 전부 다 하는거랑 새로 만드는게 그렇게까지 들어가는 노력이 많이 차이나지 않을 것이란 판단이 들어 새로 만들기로 결정했다. 사실 그런 이유들과 별개로 그냥 다시 만들고 싶었던 것도 있다.

    재작성 과정

    블로그 글 마이그레이션

    가장 먼저 사용한 스타터 템플릿을 그대로 사용하여 글부터 옮겨오자고 마음먹었다. S3에 올라가 있는 블로그 글과 메타 정보를 S3로부터 GitHub으로 (즉 먼저 로컬 리포지토로) 옮겨와야 했다. 메타 정보는 Gatsby가 이해하는 frontmatter 형태로 넣어주어야 했다.

    이 작업은 간단한 스크립트로 해결했다.

    필요 없는 부분 제거

    스타터 템플릿은 bulma를 사용하고 있었는데, 나는 이 정도 규모에선 프레임워크 없이 직접 스타일을 짜는 것을 선호한다. 스타터에 있는 스타일과 필요없는 라이브러리를 걷어내고, 추가로 사용하지 않을 컴포넌트도 삭제했다.

    스타일을 걷어낸 페이지의 모습

    스타일을 걷어낸 페이지의 모습

    스타일링 추가

    밑바탕이 준비가 되었으니, 중요하고 큰 페이지/컴포넌트부터 작업을 시작했다. 기존 블로그를 레퍼런스로 삼되 완전히 똑같이 옮기겠다는 강박은 없이 진행했다.

    글 목록 페이지 비포/애프터

    글 목록 페이지에 스타일을 입히기 시작

    글 목록 페이지에 스타일을 입힌 모습

    글 본문 페이지 비포/애프터

    스타일을 입히기 전의 글 본문 페이지

    스타일을 입힌 후의 본문 페이지

    소개 페이지 비포/애프터

    처음에 임시로 넣어둔 소개 페이지

    기존 사이트를 참고해 새로 만든 소개 페이지

    모든 컴포넌트 구현이 끝난 후엔 다시 한 번 페이지를 보며 자잘한 스타일 버그를 잡고 더 깔끔해 보이도록 가다듬는 작업을 진행했다.

    눈에 보이지 않는 작업

    마지막으로, 보이는 영역은 얼추 완성된 것 같은 시점에서 눈에 보이지 않는 작업을 진행했다.

    먼저 Route53에서 GitHub Pages의 CloudFlare 배포판을 가리키던 DNS 레코드를 netlify를 가리키도록 수정하고, 배포 후에 발견한 SSR 관련 버그를 잡았다. 이렇게 도메인을 엮고 Let’s Encrypt를 통해 발급받은 인증서로 HTTPS를 세팅하는게 너무 간단히 이루어져서 좀 놀랐다.

    다음으론 OpenGraph, Twitter 등 소셜 미디어 공유에 사용되는 메타 태그를 추가하고 Google Analytics를 추가하는 등의 눈에 보이지 않는 작업을 진행했다. 메타 태그 셋업에는 react-helmet 을 사용했고, GA는 공식 플러그인이 존재해서 쉽게 붙일 수 있었다.

    기술 선택

    이번에 블로그를 만들면서 사용한 기술 목이다. 각 분야 최고의 툴을 찾기 위해 오래 리서치하기보단 빠르게 익혀 적당히 쓸만한 결과를 만들 수 있는 툴이라는 생각이 들면 바로 도입하는 식으로 작업했다.

    • GatsbyJS: 생산성 차이가 크지 않다면 로우레벨 인터페이스를 노출하는 쪽을 선호해서 Next.js를 직접 쓰는 안도 고려해봤지만 최종적으론 정적 페이지에 더 초점을 맞춘 Gatsby를 선택했다. GraphQL에 살짝 발을 담궈보고 싶기도 했다.
    • styled-components: 회사에서 사용중이기도 하고, CSS-in-JS 라이브러리 중 가장 널리 퍼져있는 라이브러리라 선택했다.
    • netlify + netlify-cms: 무료고 Gatsby와 잘 붙는 툴이어서 선택했다. CMS를 살펴보니 필요한 기능은 얼추 있는 것 같고, 그러면서도 과도하게 복잡하지 않은 면도 마음에 들었다. 사실 CMS는 처음 써보는 것이라 뭘 중점적으로 봐야하고 뭐가 좋은지 감이 별로 없다.
    • TypeScript : Gatsby의 타입스크립트 플러그인이 babel 트랜스파일링을 사용해서 실제 타입체크를 위해선 별도로 tsc를 돌려야하지만 개발 과정에서 IDE만으로도 많은 도움을 받을 수 있다.
    • Sass: 지금까지 늘 사용해왔던 CSS 전처리기고, 스타터에 들어있길래 일단 그대로 사용했다. 나중에 여유가 난다면 cssnext 등으로 대체하면 좋겠다.

    아직 며칠 만져보지 않았지만 사용한 기술들이 전반적으로 마음에 들었다. 특히 Gatsby의 완성도와 생태계의 풍부함, 그리고 플러그인 API를 보았을 때, 또는 netlify로 몇 초만에 커스텀 도메인을 연결하고 바로 HTTPS 를 적용할 땐 “와우…” 소리가 절로 나왔다.

    개인적으로는 Webpack config 를 만지지 않고도 빌드 설정을 손쉽게 바꾸는 경험이 신선해서 이런 트윗도 남겼다. 남이 만들어둔 플러그인이 없는 케이스를 맞닥뜨리면 좀 귀찮아 지겠지만 그다지 자주 있을 일은 아닌 듯 해 괜찮을 것 같다.

    신경쓴 점

    홈페이지에서 블로그로

    지난 버전을 만들 때에는 ‘홈페이지’를 만든다는 인식이 강했다. 그래서 대문은 나를 소개하는 about 페이지로 두고, 글 목록은 하나의 메뉴로 만들었다. 다른 메뉴로는 palette 가 있었는데, 꾸준히 UI/그래픽스 작업을 진행해 나만의 갤러리를 만들겠다는 야심찬 계획이었다. (한창 구글의 UX Engineer 김종민님이 멋있다고 생각했던 때라 그런 것 같다)

    몇 달을 굴려보니 명확해진 사실들:

    • 나는 글을 쓰는 것은 좋아하는 만큼 UI/그래픽스 쪽에 관심이 맞지 않다 (글 목록과 팔레트를 같은 층위의 메뉴로 두기엔 무게감이 너무 다르다)
    • 사용자가 사이트에 들어왔을 때 매번 똑같은 대문 페이지를 보여주고, 굳이 한 번 더 클릭해야 글 목록이 보이는 건 좋은 UX가 아니다.

    이 깨달음을 바탕으로, 이번엔 ‘블로그’를 만든다는 느낌으로 작업을 진행했다. 대문에 글 목록을 보여주고, 업데이트가 거의 되지 않던 palette 메뉴는 삭제했다. 만약 언젠가 블로그로 커버할 수 없는 유즈케이스가 분명하게 드러난다면 그 때 구조를 다시 고민해 볼 생각이다.

    야크 털 깎기 방지

    작업을 시작할 때부터 다음주 월요일에 글을 써서 올린다는 빠듯한 시간 제한의 목표를 갖고 있었다. 이 시간을 지키는게 가장 중요하다고 생각해서, 야크 털 깎는 작업을 피하려 꾸준히 노력했다. 작업을 하다가 ‘이게 안 되도 글 써서 올리고 공개하는데 아무 문제 없지 않나?’ 라는 생각이 드는 순간 바로 백로그로 넘기고 꼭 필요한 작업으로 넘어갔다. 지금 손봐야할 구석이 많지만 덕분에 시간 제한을 맞췄으니 결과적으로 성공.

    앞으로

    내놓기에 너무 부끄럽지 않은 수준으로만 급하게 만들어서 내놓는 것이라 내적으로도 외적으로도 개선하고 싶은 부분이 많다. 까먹지 않기 위해 GitHub Issue를 사용해 관리하고 있는데, 그 중 몇 가지만 적어본다.

    너무 뻔한 스타일 탈피

    지금 디자인은 나름 깔끔하고 글에 집중할 수 있는 것 같아, 그건 마음에 든다. 하지만 하다못해 사용된 색도 대부분 무채색이고, 전반적으로 이 사이트를 차별화하는 포인트가 없다보니 너무 요새 어디서나 볼 수 있는 흔한 사이트가 되어버린 것이 아쉽다. 한 마디로 보는 재미가 없다.

    기성 툴을 사용하지 않고 굳이 직접 블로그를 만들 때 가장 큰 장점은 다양한 시도를 자유롭게 해 볼 수 있다는 것이라 생각한다. 지금의 구현은 그 장점을 못 살리고 있는 듯 하다. 방문자가 이 사이트를 생각하면 바로 떠오를만한 무언가, 보고 즐거워할 무언가를 만들고 싶다.

    path 관련 이슈

    netlify-cms는 리소스에 엮인 URL path를 직접 지정하는 방법을 제공하지 않는다. {{date}}-{{slug}} 와 같은 식으로 리소스 생성일자와 제목으로부터 자동으로 생성된 slug를 사용한 패턴만을 사용할 수 있는데, 짐작이 가겠지만 제목이 CJK인 경우 애로사항이 많다.

    일단은 글이 생성된 이후로는 path가 변하지 않아, 원하는 path를 제목으로 사용해 글을 만들고, 그 이후 제목을 수정하면 이슈를 피해갈 수 있는 것을 확인했다. 하지만 이건 workaround일 뿐이고, netlify-cms 쪽에 URL path 를 설정하기 위한 필드를 하나 추가하면 근본적인 문제를 확실히 해결할 수 있을 것이다.

    관련 이슈를 보면 오래동안 살아있고 원하는 사람이 많은데도 일손이 부족해 방치되어 있는 듯 한데, 난이도도 적절하고 직접적으로 필요한 기능이라 올해 안에 시간을 내서 기여를 하는 것을 목표로 잡고 있다.

    접근성

    웹 개발을 하면서 중요하다고 생각만 하면서 신경쓰지 않고 있던 (많은) 점들 중 하나가 접근성이다. 일정에 치이는 회사 업무에서는 힘들더라도 이렇게 개인 프로젝트에서라도 시작하면 좋을 것 같아 할 일 목록에 올려 두었다.

    일단은 가장 기본적으로 키보드로 모든 내비게이션이 가능하게 하는 것부터 시작했다. 추후에는 ARIA 관련 속성 추가, 키보드 내비게이션을 더 쉽게 하기 위한 컴포넌트 추가 등 다양한 개선안을 적용해보고자 한다.

    소회

    얼마 전 매주 글을 한 편씩 쓰기로 다짐하고 공표했다. 그러려면 당연히 일정 시간을 글쓰기에 할당해야 할 것 같아 지난 주부터 1시간 일찍 출근해서 블로그를 만들었다. 하루를 일찍 시작하니 상쾌했고, 늦게 퇴근하거나 놀거나 하는 이유로 아무것도 안 하고 넘어가는 일이 없어 좋았다.

    즐거움에 관한 ‘재수의 연습장’ 계정의 그림 (출처 https://twitter.com/jessoosketch/status/1046783353523855360)

    얼마 전, 위 그림과 글이 담긴 연습장 사진을 인상깊게 보았다. (원 출처 트윗) 새로운 기술로 생산적인 일을 하는 재미가 있으니 확실히 유튜브에 낭비하는 시간이 줄어들었다. 일찍 출근해서 글쓰기나 블로그 개선에 시간과 즐거움을 쓰는 습관은 계속 가져가려 한다.

    원래 스크린샷도 잔뜩 첨부하고 근사하게 써서 올릴 생각이었다. 그런데 블로그 만드는 작업이 늦어져 주말에서야 글을 쓰기 시작했다. 글을 쓸 장소를 만드느라 글을 제 시간에 못 내놓는 것은 주객전도인 듯해 적당히 써서 올린다. 별 내용이 없어서 뭘 이런걸로 글까지 쓰냐… 싶기도 하지만 요새 너무 멋있는 척 할 필요 없다는 걸 깨닫고 있어서 뭐.

    앞으로도 완벽하지 않아도 좋으니 꾸준히만 쓰는걸로!