Coderifleman's blog

frontend development stories.

  • 프런트엔드 엔지니어를 위한 베지에 곡선(Bézier Curves) - 2편」을 포스팅한 후 시간이 꽤 지났다. 어디까지 이야기했더라? 기억도 가물가물하다. 최근에 강의를 시작하면서 여유가 없었다는 핑계를 대보지만, 뭐가 됐든 게을러서 그렇다. 3편을 기다리신 분이 있었다면 죄송할 따름이다.

    글과 관련 없는 얘기는 이쯤 하자. 2편에서는 에버리징과 블렌딩 그리고 인터폴레이션(Interpolation)이라고 부르는 보간을 소개했다. 베지에 곡선을 이해하기 위한 기초 지식이었으며 이 개념만 이해하고 있으면 나머지는 쉽게 이해할 수 있다.

    1차 베지에 곡선

    우리는 이미 1차 베지에 곡선(Linear Bezier Cuvers)을 경험했다. 2편의 복합 데이터 블렌딩보간 절에서 보여준 예제가 바로 1차 베지에 곡선이다. 차이점이 있다면 단순히 평면상에서 곡선을 그리는 게 아니라 직교좌표계 상에서 그린다는 것이다.

    1차 베지에 곡선
    <그림 1. 1차 베지에 곡선>

    1차 베지에 곡선은 조절점(Control point) 두 개로 그린다. 아주 간단하지만, 굴곡이 없는 선형이다(직선도 곡선에 포함된다는 사실을 잊지 말자).

    2차 베지에 곡선

    그럼 2차 베지에 곡선(Quadratic Bézier Curves)을 그려보자. 2차 베지에 곡선은 조절점 3개를 이용해 그린 곡선을 말한다.

    2차 베지에 곡선
    <그림 2. 2차 베지에 곡선>

    3개의 조절점 A, B, C를 이용해 그린 두 개의 직선 즉, 두 개의 1차 베지에 곡선이 있다. 그리고 이 곡선에서 보간되는 점 E와 F도 있다. 이때 점 E와 F를 이용해 또 다른 직선을 그릴 수 있고 이 직선에서 보간되는 점 P도 추가할 수 있다. 이제 점 E와 F 그리고 P를 보간하면 P의 행적이 곡선을 만들어 낸다(이해가 되지 않는다면 「중학생도 알 수 있는 베지에 곡선」을 참고한다).

    그럼 이제 2차 베지에 곡선을 직접 그려보도록 하자. 선분에서 블렌딩 되는 점 P를 구하는 공식은 다음과 같다(자세한 내용은 「프런트엔드 엔지니어를 위한 베지에 곡선(Bézier Curves) - 2편」을 참고). 이때 s = 1 - t다.

    P = (s * A) + (t * B)

    그림 2를 보면 알 수 있듯이 2차 베지에 곡선을 그리기 위해서는 점 E와 F 그리고 P를 보간해야 한다. 점 E는 조절점 A와 B를 이용해 구할 수 있고, 점 F는 조절점 B와 C를 이용해 구할 수 있다. 그리고 점 P는 다시 점 E와 F를 이용해 구할 수 있다.

    E = (s * A) + (t * B)
    F = (s * B) + (t * C)
    P = (s * E) + (t * F)

    이 공식을 자바스크립트 코드로 옮겨보자. 여기에서는 구현에 있어 몇 가지 중요한 함수만 소개한다. 전체 코드는 코드펜(CodePen)에 작성해 놓은 예제를 참고한다.

    먼저 blender()는 점 A와 점 B 그리고 가중치 t를 전달받아 블랜딩한 결괏값을 반환하는 함수다.

    function blender(A, B, t) {
        if (t === 0) {
            return A;
        }
    
        if (t === 1) {
            return B;
        }
    
        return ((1 - t) * A) + (t * B); // or A + t * (B - A)
    }

    이때 blender()는 좌표 하나에 대한 연산만 책임지므로 x, y 좌표를 연산하기 위해 blend()를 작성한다.

    function blend(x1, x2, y1, y2, t) {
      const x = blender(x1, x2, t);
      const y = blender(y1, y2, t);
    
      return {x, y};
    }

    다음으로 blend()를 이용해 점 A와 점 B의 좌표를 전달해 점 E의 좌푯값을 구하고 점 B와 점 C의 좌표를 전달해 점 F의 좌표를 구한다. 그리고 다시 점 E와 점 F의 좌표를 전달해 점 P의 좌표를 구하는 방식으로 공식을 구현한다.

    interpolateBtn.addEventListener('click', function() {
      // Start the interpolation.
      raf(function(t) {
        const posE = blend(posA.x, posB.x, posA.y, posB.y, t);
        const posF = blend(posB.x, posC.x, posB.y, posC.y, t);
        const posP = blend(posE.x, posF.x, posE.y, posF.y, t);
        ...
      }, 1000);
    });

    아래 데모를 실행해 보자. 점 P가 보간되면서 그려진 곡선을 2차 베지에 곡선이라고 한다.

    See the Pen qrpYwj by Uyeong Ju (@uyeong) on CodePen.

    수식 정리

    우리가 2차 베지에 곡선을 위해 사용한 수식은 다음과 같다.

    E = (s * A) + (t * B)
    F = (s * B) + (t * C)
    P = (s * E) + (t * F)

    하지만 이 수식은 조금 장황하며 자바스크립트 코드상에서도 함수 호출이 빈번한 상태다. 이 수식을 방정식으로 좀더 간결하고 효율적으로 표현할 수 있다. 일단 연산식에 있는 괄호를 없애고 좀더 간략하게 수식을 표현한다.

    E(t) = sA + tB
    F(t) = sB + tC
    P(t) = sE(t) + tF(t)

    이번엔 중학생 때 배워본 몇 가지 곱셈 공식 사용하여 세 줄로 표현한 수식을 한 줄로 작성하고 이차방정식으로 정리한다.

    P(t) = s(sA + tB) + t(sB + tC)
    P(t) = (s²)A + (st)B + (st)B + (t²)C
    P(t) = (s²)A + 2(st)B + (t²)C

    자, 몇 가지 규칙을 더 추가하자. t가 0이라면 P는 항상 A와 같으며 다음과 같이 증명할 수 있다.

    P(t) = (s²)A + 2(st)B + (t²)C
    P(t) = (1²)A + 2(1 * 0)B + (0²)C
    P(t) = (1)A + 2(0)B + (0)C
    P(t) = (1)A
    P(t) = A

    다시 t가 1이라면 P는 항상 C와 같으며 다음과 같이 증명할 수 있다.

    P(t) = (s²)A + 2(st)B + (t²)C
    P(t) = (0²)A + 2(0 * 1)B + (1²)C
    P(t) = (0)A + 2(0)B + (1)C
    P(t) = (1)C
    P(t) = C

    이제 정리한 수식을 자바스크립트로 작성해보자. 함수명은 quadBezier로 짓고 2차 베지에 곡선임을 나타낸다.

    function quadBezier(A, B, C, t) {
      if (t === 0) {
        return A;
      }
      
      if (t === 1) {
        return C;
      }
      
      const s = 1 - t;
      
      return Math.pow(s, 2) * A + 2 * (s * t) * B + Math.pow(t, 2) * C;
    }

    이렇게 작성한 함수는 다음과 같이 사용할 수 있다.

    interpolateBtn.addEventListener('click', function() {
      // Start the interpolation.
      raf(function(t) {
        const x = quadBezier(posA.x, posB.x, posC.x, t);
        const y = quadBezier(posA.y, posB.y, posC.y, t);
        ...
      }, 1000);
    });

    여기까지 1차 베지에 곡선과 2차 베지에 곡선에 대해 알아봤다. 다음 편에서는 3차 베지에 곡선을 소개하고 이 곡선을 애니메이션에서 어떻게 활용하는지 소개하겠다.

    참고

  • 베지에 곡선과 관련된 수학적 증명 방법과 알고리즘은 1959년 프랑스의 자동차 업체 시트로엥(citroen)에서 근무하던 물리학자이자 수학자인 파울 드 카스텔조(Paul de Casteljau)가 최초 고안했다. 다른 말로 카스텔조 곡선(Casteljau curve)이라고 부른다.

    하지만 시트로엥의 정책으로 인해 카스텔조가 얻은 성과가 1974년까지 발표되지 못했고 1962년에 프랑스 자동차 회사 르노(Renault)에서 근무하던 엔지니어 피에르 베지에(Pierre Bézier)가 자동차를 디자인하는 과정에서 이 곡선을 독자적으로 개발해 사용하면서 그의 이름으로 널리 알려지게 된다.

    정의

    베지에 곡선은 간단히 말해 복수의 조절점(Control point)을 이용해 매끄러운 곡선을 그릴 수 있는 가장 일반적인 매개 변수 곡선(Parametric curve) 이다. 매개 변수 곡선이란 매개 변수를 사용해 함수를 일반화하여 곡선을 그려내는 방법을 말한다.

    베지에 곡선의 조절점과 가이드 포인트
    <그림 1. 베지에 곡선의 조절점과 가이드 포인트>

    조절점이란 곡선의 모양을 결정하는 데 사용되는 점의 집합 또는 구성원을 뜻하며 가이드 포인트는 곡선의 모양을 변경시킬 수 있는 조절 가능한 점을 뜻한다. 그림 1의 점을 왼쪽부터 차례대로 P0, P1, P2, P3라고 할 때 보통 P0와 P3는 고정해두고 P1과 P2를 조절해 곡선을 변형한다. 이때 이 P1과 P2를 가이드 포인트라 한다.

    베지에 곡선엔 차수가 붙는데 이 차수는 조절점의 개수에 따라 정해진다. 간단히 말해 N 개의 조절점으로 그려진 곡선을 N - 1차 베지에 곡선이라 한다. 예를 들어 그림 1은 조절점이 4개이므로 4 - 1 즉, 3차 베지에 곡선이다.

    베지에 곡선이 그려지는 개괄적인 원리는 이전에 포스팅했던 「중학생도 알 수 있는 베지에 곡선(Bézier Curves)」을 참고하자. GIF 애니메이션을 이용해 이해하기 쉽게 설명돼 있다.

    이번 편에서는 블렌딩(Blending)과 보간(Interpolcation)을 소개한다. 이 지식만 습득하면 나머지 n 차 베지에 곡선은 자연스럽게 이해할 수 있다.

    에버리징과 블렌딩

    점 A와 점 B의 중앙
    <그림 2. 점 A와 점 B의 중앙>

    자, 위 그림 2와 같이 서로 떨어져 있는 점 A와 점 B가 있다고 해보자. 이때 점 P를 이 두 점 사이의 평균 즉, 선분의 중앙에 두고 싶다면 어떻게 해야 할까?

    P = (A + B) / 2

    위처럼 간단히 평균을 구해 중앙에 둘 수 있다. 이 수식을 조금 다르게 전개해보자.

    P = (A + B) / 2
      = (A + B) * ½
      = ½A + ½B
      = .5A + .5B
      = (.5 * A) + (.5 * B)

    위 수식을 이용한 연산을 블렌딩(Blending)이라고 표현한다. 지정된 각각의 비율에 맞춰 적절히 혼합하는 것이다. 자, 이제 같은 값이 아닌 가중치(Weights)를 줘서 블렌딩해보자.

    점 A와 점 B의 블렌딩 비율 조절
    <그림 3. 점 A와 점 B의 블렌딩 비율 조절>
    P = (.35 * A) + (.65 * B)

    이번엔 A는 0.35(35%), B는 0.65(65%)로 비율을 조정해 블렌딩했다. 이때 두 비율의 합은 당연하겠지만 1(100%)이 돼야 한다. 이것은 반드시 지켜져야 할 필수 조건이다. 이어서 비율 값을 다음과 같이 일반화해보자.

    P = (s * A) + (t * B)

    s는 A의 비율을, t는 B의 비율을 나타낸다. 만약 s가 높으면 t는 낮아지고 반대로 t가 높으면 s는 낮아질 것이다. 잠깐, s와 t는 서로 영향을 주며 두 수의 합은 항상 1이 돼야 한다. 그렇다면 s는 1 - t와 같다고 할 수 있다.

    P = ((1-t) * A) + (t * B)

    이제 변수 t 하나만으로 비율을 조정해 블렌딩할 수 있다. 이 수식은 다음과 같이 표현될 수 있다.

    P = ((1 - t) * A) + (t * B)
      = (1 - t) * A + t * B
      = A - tA + tB
      = A + t(-A + B)
      = A + t(B - A)

    이 글에서는 수식 P = (s * A) + (t * B)를 사용해 설명을 이어가겠다. 다시 한번 말하지만 s = 1 - t다. 여기에 몇 가지 규칙이 추가된다. 만약 변수 t가 0이라면 P는 항상 A와 같으므로 다음과 같이 표현될 수 있다.

    P = ((1 - t) * A) + (t * B)
      = ((1 - 0) * A) + (0 * B)
      = (1 * A) + (0 * B)
      = A

    또 변수 t가 1이라면 P는 항상 B와 같으므로 다음과 같이 표현될 수 있다.

    P = ((1 - t) * A) + (t * B)
      = ((1 - 1) * A) + (1 * B)
      = (0 * A) + (1 * B)
      = A + B - A
      = B

    이제 수식과 몇 가지 규칙을 참고하여 블렌딩하는 자바스크립트 함수를 작성해보자.

    const A = 20;
    const B = 198;
    
    function blender(A, B, t) {
        if (t === 0) {
            return A;
        }
    
        if (t === 1) {
            return B;
        }
    
        return ((1 - t) * A) + (t * B); // or A + t * (B - A)
    }
    
    const blend = blender.bind(null, A, B);
    
    console.log(blend(.0)); // 20
    console.log(blend(.2)); // 55.6
    console.log(blend(.4)); // 91.2
    console.log(blend(.6)); // 126.8
    console.log(blend(.8)); // 162.4
    console.log(blend(1));  // 198

    See the Pen qRBdvb by Uyeong Ju (@uyeong) on CodePen.

    복합 데이터 블렌딩

    이번에는 「에버리징과 블렌딩」 절에서 이해한 수식을 이용해 복합 데이터(Compound data)를 블렌딩해보자. 블렌딩 수식만 잘 활용하면 2차원 또는 3차원 같은 복합적인 데이터도 쉽게 블렌딩할 수 있다.

    Px = (s * Ax) + (t * Bx)
    Py = (s * Ay) + (t * By)
    Pz = (s * Az) + (t * Bz)

    3차원인 경우 위처럼 개별적으로 블렌딩한 후 연산된 값을 조합해 사용한다.

    2차원에서 점 A와 점 B의 중앙
    <그림 4. 2차원에서 점 A와 점 B의 중앙>

    전 절에서는 점 A와 점 B가 동일 선상에 놓여있는 1차원적 상황이었지만 이번엔 그림 4 처럼 2차원 상황에서 P를 구해보자. 2차원에서는 x와 y 좌표로 점의 위치가 결정된다. 따라서 x와 y를 개별적으로 블렌딩한 후 구해진 값을 조합하면 P의 위치를 구할 수 있다.

    2차원에서 점 P의 위치 구하기
    <그림 5. 2차원에서 점 P의 위치 구하기>
    Px = (s * Ax) + (t * Bx)
    Py = (s * Ay) + (t * By)
    P = {Px, Py}

    위 수식은 자바스크립트 코드로 다음과 같이 표현할 수 있다.

    const Ax = 20;
    const Ay = 144;
    const Bx = 198;
    const By = 72;
    
    const blendX = blender.bind(null, Ax, Bx);
    const blendY = blender.bind(null, Ay, By);
    
    // t = .5
    // P = { blendX(t), blendY(t) }

    See the Pen EZxVVV by Uyeong Ju (@uyeong) on CodePen.

    보간

    마지막으로 보간(Interpolation)의 개념을 짤막하게 소개한다. 러핑(Lerping)이라고도 부르는 보간은 시간이 지남에 따라 블랜드 가중치를 변경하여 블렌딩을 수행하는 것을 말한다. 시간은 멈춰있지 않고 지속해서 흐르는 특징이 있으며 블랜드 가중치는 이 흐르는 시간에 의해 결정된다.

    쉽게 말해 특정 값으로 블렌딩하는 게 아닌 지속해서 흐르는 시간에 근거해 블렌딩 하는 것이다. 이러한 과정은 대개 update()로 표현된다. 아래 자바스크립트 코드를 보자.

    function interpolator(Ax, Bx, Ay, By, duration) {
      return function(update) {
      	 // x, y 블랜드 함수 준비
        const blendX = blender.bind(null, Ax, Bx);
        const blendY = blender.bind(null, Ay, By);
        
        ... 중략 ...
        
        function step(timestamp) {
          
          ... 중략 ...
          
          // 현재 시간에 해당하는 진행 값 즉, t 값 연산
          const pastTime = timestamp - startTime;
          let progress = pastTime / duration;
    
    	   // t 값을 이용해 블렌딩하고 update 콜백 함수 호출
          update(blendX(progress), blendY(progress)); // Blending...
    
          ... 중략 ...
          
          requestAnimationFrame(step);
        }
        
        requestAnimationFrame(step);
      }
    }
    
    const interpolate = interpolator(Ax, Bx, Ay, By, 1000);
    
    interpolate(function(nx, ny) {
    	// 1초간 Interpolating.
    	// P = {nx, ny}
    });

    우선 requestAnimationFrame()를 사용해 지정한 시간 만큼 흐르게 한다. 그리고 현재 시각에 해당하는 진행 값 즉, t를 구한 후 이 값을 근거해 블렌딩한다.

    See the Pen dNbgQp by Uyeong Ju (@uyeong) on CodePen.

    보간은 페이드인, 아웃 같은 애니메이션 처리나 3D 게임에서의 객체 움직임 그리고 오디오 크로스페이드 처리 등에 유용하게 사용된다.

    여기까지 2편을 마치고, 다음 편에서 이 지식을 바탕으로 1, 2차 베지에 곡선을 소개하겠다.

    참고

  • 퇴근 후 여느 때와 마찬가지로 PlayStation 4 전원에 손이 향하는 순간, 오랫동안 관리하지 못한 react-preloader-icon 컴포넌트가 돌연 떠올랐다. react-preloader-icon은 SVG Loaders의 아이콘을 React 컴포넌트로 옮기고 있는 작은 사이즈의 프로젝트다.

    SVG Loaders에 디자인된 아이콘은 12개밖에 안되지만 게으른 나머지 아직 2개밖에 옮기지 못했다. 그래서 게임은 잠시 제쳐두고 Spinning과 Puff 아이콘을 한번 React 컴포넌트로 옮겨보기로 했다.

    Spinning과 Puff
    <그림 1. Spinning과 Puff>

    SVG와 3차 베지에 곡선

    Spinning 아이콘은 손쉽게 옮겼지만, Puff 아이콘은 좀 달랐다. 이전에 옮긴 아이콘 모두 애니메이션이 선형적(linear) 이기 때문에 신경 쓸 게 없었다. 하지만 Puff 아이콘의 SVG는 조금 다른 방식으로 만들어져 있다.

    <svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
        <g fill="none" fill-rule="evenodd" stroke-width="2">
            <circle cx="22" cy="22" r="1">
                <animate attributeName="r"
                    begin="0s" dur="1.8s"
                    calcMode="spline"
                    values="1; 20"
                    keyTimes="0; 1"
                    keySplines="0.165, 0.84, 0.44, 1"
                    repeatCount="indefinite" />
                    ... 중략 ...
            </circle>
        </g>
    </svg>

    위 코드에서 animate 엘리먼트를 살펴보자. 이 엘리먼트는 calcMode, values, keyTimes, keySplines 속성을 갖고 있다. 일단 이 속성에 관한 지식이 전혀 없기 때문에 우선 SMIL 스펙 문서를 살펴봤다.

    스펙 문서를 통해 calcMode값의 타이밍을 제어할 함수를 선택하는 속성임을 알 수 있다. 속성 값으로 “discrete”, “linear”, “paced”, “spline” 중 하나를 지정할 수 있다.

    위 코드의 calcMode 속성에는 “spline”이 지정돼 있는데 “spline”으로 지정하면 values, keyTimes, keySplines 속성과 함께 “3차 베지에 곡선“으로 값을 제어할 수 있다.

    잠깐, “3차 베지에 곡선”이라고? 다들 CSS로 애니메이션을 처리할 때 cubic-bezier라는 애니메이션 타이밍 함수를 한 번쯤 사용해본 적이 있을 것이다. cubic-bezier… 그렇다. 3차 베지에라는 뜻이다.

    CSS와 3차 베지에 곡선

    cubic-bezier 애니메이션 타이밍 함수는 x, y, x`, y` 즉, 4개의 값을 인자로 전달받아 에니메이션의 타이밍을 조절한다. x, y는 첫 번째 가이드 포인트(Guide Point)의 좌표, x`, y`은 두 번째 가이드 포인트의 좌표다. 가이드 포인트란 곡선의 형태에 영향을 주는 조절 가능한 점을 뜻한다.

    3차 베지에 곡선의 가이드 포인트
    <그림 2. 3차 베지에 곡선의 가이드 포인트>

    3차 베지에 곡선이란 이 두 가이드 포인트의 위치에 따라 그려지는 곡선을 말한다. cubic-bezier는 이 곡선을 이용해 에니메이션의 타이밍을 조절한다(이 속성은 cubic-bezier.com, CSS3 Bezier Curve Tester, desmos에서 간단히 테스트해 볼 수 있다).

    cubic-bezier를 이용해 선언한 Easing 함수 셋
    <그림 3. cubic-bezier를 이용해 선언한 Easing 함수 셋>

    우리가 알고 있는 easeInSine, easeInQuad 등과 같은 Easing 함수(참고)는 모두 이 베지에 곡선을 이용해 미리 만든 일종의 셋이다.

    한걸음 더…

    자, 본래 이야기로 다시 돌아와서…

    SVG의 calcMode="spline"의 의미를 살펴봤으니 bezier-easing같은 npm 모듈을 사용해 Puff 아이콘을 React 컴포넌트로 옮기면 된다. 하지만 커밋을 완료하고 따듯한 이불 속에서 편안한 마음으로 잠자리에 들기엔 모르는 것이 너무나 많다.

    왜 베지에 곡선이라 부를까? 무엇을 근거로 1차, 2차, 3차라고 나눌까? 또, 곡선이 그려지는 원리와 공식은 무엇일까? 몇 가지 물음이 잠 못 이루게 했고 결국 이 주제로 글을 작성하게 됐다.

    그럼 다음 편부터 본격적으로 베지에 곡선에 관해서 연재하도록 하겠다.

    참고

  • 읽기전에...

    이 문서는 일본어 「中学生でもわかるベジェ曲線」를 번역한 글입니다.

    “베지에 곡선”을 이용해 렌더링하게 되면 꽤 재미있고 편안하게 그림을 그릴 수 있다. 오늘은 이를 사용하는 누구라도 그 원리를 이해할 수 있도록 설명하고자 한다.

    “베지에 곡선”이라는 것은 매끄러운 곡선을 그리기 위한 것이지만 설명은 우선 단순한 직선부터 시작하겠다. 아래 그림 1 처럼 직선에서의 점의 움직임이 모든 “베지에 곡선”의 기본이 되기 때문이다.

    1차 베지에 곡선
    <그림 1. 1차 베지에 곡선>

    하나의 직선이 있고 그 위를 점 M이 일정 속도로 이동하고 있다. 이 점 M의 궤적은 당연하지만 단순한 직선으로 그려진다. 좋다. t선분 위를 비율적으로 얼마나 나아갔는지를 나타내는 수치다.

    여기에 선을 하나 더 추가하고 그 위에 M처럼 이동하는 점을 놓아보자. 그리고 원래의 점 MM0로, 새로운 점을 M1으로 부르자. M0M1이 움직이는 규칙은 이전과 같다. M1이라는 점이 하나 더 늘었다 하더라도 특별히 복잡해질 것은 없다.

    2차 베지에 곡선
    <그림 2. 2차 베지에 곡선>

    자, 여기에서 M0M1을 잇는 선을 하나 더 그을 수 있다. 그 선은 M0M1이 이동하면 자연스럽게 함께 움직이게 된다. 이제 그 선에 주목해보자. 그 선 위에 M0M1처럼 일정 속도로 이동하는 점을 놓을 수 있다. 그 점을 B라고 하자. 그리고 점 B가 그리는 궤적을 살펴보자. 그렇다. 점 B가 그리는 궤적을 2차 베지에 곡선(Quadratic Bezier Curve)이라고 한다.

    P0, P1 등 을 조절점(Control Point)이라고 한다. 이제 조절점을 하나 더 늘린 “베지에 곡선”을 상상해보자.

    아래 그림 3을 보면 새로운 점 P3가 추가돼 있고 P2P3를 선으로 잇고 있다. 그리고 M0M1과 같이 그 선 위를 이동하는 점을 생각해 볼 수 있다. 그 점의 이름은 M2라 하자.

    3차 베지에 곡선
    <그림 3. 3차 베지에 곡선>

    자, 2차 베이제 곡선에서는 P0, P1, P2의 조합으로 점 B의 위치를 정할 수 있었다. 그렇다면 마찬가지로 P1, P2, P3의 조합으로도 비슷하게 점의 위치를 정할 수 있을 것이다.

    이전에 부르던 점 BB0라고 부르기로 하고 P1, P2, P3의 조합으로 정해지는 새로운 점을 B1이라고 부르기로 하자. 그렇게 하면 이전과 마찬가지로 점 B0와 점 B1을 잇는 선과 그 선 위를 일정한 속도로 움직이는 점을 다시 생각해 볼 수 있는데 이때 그 점이 그리는 궤적을 3차 베지에 곡선(Cubic Bezier Curve)이라고 한다.

    여기에서 끝이 아니다. 조절점을 한 개 더 늘려 P0, P1, P2, P3 조합과 P1, P2, P3, P4 조합으로 결정되는 선과 그 선을 일정 속도로 이동하는 점 B2를 추가하고 B1B2를 잇는 선을 놓고 그 위를 지나가는 또 다른 점을 추가한 후 그 점이 그리는 궤적을 살펴볼 수 있다. 이런 식으로 곡선은 얼마든지 복잡하고 다양하게 만들 수 있다. P0부터 P4로 정해지는 곡선은 4차 베지에 곡선(Quartic Bezier Curve)이라고 부른다. 하지만 조절점을 5개 이상 늘려도 실용적인 측면에서 특별한 이점이 없으므로 일반적으로 3차 베지에 곡선까지 사용된다.

    여기까지 간단하게 “베지에 곡선”에 관해 이야기했다. 한마디로 “베지에 곡선”이란 선분 위를 일정 속도로 움직이는 점과 그러한 점과 점을 잇는 또 다른 선분, 그리고 그 위를 일정 속도로 이동하는 또 다른 점 등을 조합해 최종적으로 특정 점이 그리는 궤적을 이용해 곡선을 그려내는 방법을 뜻한다. 이해하는 데 도움이 됐으리라 기대한다.

  • 얼마전에 기능 개발을 하다가 애니메이션을 다룰 일이 생겼다. 처음엔 CSS를 이용했지만, IE8을 지원하고자 “raf“라는 폴리필 라이브러리를 이용해 requestAnimationFrame(이하 raf)으로 개발했다. raf의 대략적인 형태는 다음과 같다(API의 자세한 설명은 MDN의 window.requestAnimationFrame()을 참고).

    var start = null;
    
    function step(timestamp) {
      if (!start) {
        start = timestamp;
      }
    
      var progress = timestamp - start;
    
      // Use progress to do something.
    
      if (progress < 1500) {
        window.requestAnimationFrame(step);
      }
    }
    
    window.requestAnimationFrame(step);

    코드를 보면 알겠지만 그렇게 직관적인 편은 아니다. raf를 쓸 때마다 이런 식으로 코드를 작성하긴 싫었다. 그래서 사용하기 편하게 raf를 랩핑한 객체 하나를 만들었다(여기서 시작한 작은 프로젝트가 있다 - StepperJS).

    class Stepper {
      start(options) {
        const {
          duration = 0,
          easing = linear, // is easing function.
          step = () => {}
        } = options;
    
        let startTime = 0;
    
        const stepping = (timestamp) => {
          if (!startTime) {
            startTime = timestamp;
          }
    
          const pastTime = timestamp - startTime;
          const progress = pastTime / duration;
    
          if (pastTime >= duration) {
            step(1);
            return;
          }
    
          step(easing(progress));
    
          window.requestAnimationFrame(stepping);
        };
    
        window.requestAnimationFrame(stepping);
      }
    }

    이렇게 작성한 Stepper 객체는 다음과 같이 사용할 수 있다.

    const stepper = new Stepper();
    
    stepper.start({
      duration: 1500,
      step: (n) => {
        element.style.left = `${150 * n}px`
      }
    });

    See the Pen yVpNLo by Uyeong Ju (@uyeong) on CodePen.

    개인적으로 생각했을 때 raf를 곧바로 사용하는 것보다 더 직관적이고 편해 보인다(물론 다른 의견을 가진 사람이 있을 수도 있다). 이제 Stepper 객체를 어떻게 테스트할 수 있을지 살펴보자.

    Stepper는 지정한 durationeasing에 따라 현재 시점에 해당하는 n 즉, progress 값을 콜백 함수에 전달하는 단순한 역할을 담당한다. 그렇다면 다음과 같이 테스트 케이스를 작성할 수 있을 것 같다.

    should call step callback with the current progress by duration and easing.

    음, 확실히 유닛 테스트로써는 손색없지만 필자는 사용자 관점에서 서술하는 걸 좋아하니 이렇게 고쳐보자.

    The user should be able to know the current progress through the start method of Stepper

    이를 어떻게 검증할 수 있을까? 필요한 값을 설정하고 start 메서드를 호출한 후 특정 시간으로 옮긴(tick) 다음 “n”이 기대하는 값과 일치하는지 확인하면 될 것 같다. 한번 테스트 코드로 옮겨보자.

    test('The user should be able to know the current progress through the start method of Stepper', (assert) => {
      // Given
      const stepper = new Stepper();
      const duration = 300;
      const easing = linear;
      let progress;
      
      // When
      stepper.start({
        duration,
        easing,
        step: (n) => progress = n
      });
      
      // Then
      assert(progress === ???);
    });

    다른 조건들은 어려울 게 없지만, 특정 시간으로 옮기는 행위는 그렇지 않다. 함수를 호출하는 순간 시간은 흘러 버리므로 특정 시간에 해당하는 progress 값을 비교할 수 없다.

    이처럼 테스트 환경에서 시간을 조작하고 싶을 때 사용할 수 있는 테스트 더블 라이브러리가 있다. 바로 “sinon“이다. sinon의 FakeTimersetTimeoutDate 객체 등을 덮어써서 동기적으로 시간을 조작할 수 있는 수단을 제공한다. 이것을 사용해보자.

    const clock = sinon.useFakeTimers();
    
    test('The user should be able to know the current progress through the start method of Stepper', (assert) => {
        // Given
        const stepper = new Stepper();
        const duration = 300;
        const easing = linear;
        const step = sinon.spy();
    
        // When
        stepper.start({
            duration,
            easing,
            step
        });
    
        clock.tick(0);
        clock.tick(250);
    
        // Then
        assert(step.args[1][0].toFixed(2) === linear(250 / 300).toFixed(2));
    });

    See the Pen PbEaMp by Uyeong Ju (@uyeong) on CodePen.

    당연한 얘기겠지만 TypeError가 발생한다. raf는 시간이 아닌 repaint 시점을 기준으로 호출되며 독자적으로 타임스템프를 계산해 콜백에 전달하므로 sinon의 FakeTimer로 조작할 수 없다. 따라서 동기적으로 호출한 args 프로퍼티에 쌓인 값이 없으므로 에러가 발생하는 것이다.

    그렇다면 어떻게 해야 할까. 고맙게도 누군가 raf를 Stub한 “raf-stub“을 개발해 배포해놨다. 이 Stub을 사용해 테스트를 다시 작성해보자.

    const stub = createStub();
    
    sinon.stub(window, 'requestAnimationFrame', stub.add);
    
    test('The user should be able to know the current progress through the start method of Stepper', (assert) => {
        // Given
        const stepper = new Stepper();
        const duration = 300;
        const easing = linear;
        const step = sinon.spy();
    
        // When
        stepper.start({
            duration,
            easing,
            step
        });
    
        stub.step(1, 0);
        stub.step(1, 250);
    
        // Then
        assert(step.args[1][0].toFixed(2) === linear(250 / 300).toFixed(2));
    });

    See the Pen LbeJZz by Uyeong Ju (@uyeong) on CodePen.

    테스트가 통과한다. raf를 사용해 작성한 코드를 테스트하려고 할 때 다소 막막할 수 있다. 하지만 sinon과 Stub을 적절히 사용한다면 손쉽게 테스트할 수 있다.

    여기까지 raf를 테스트하는 방법을 소개했다. 비슷한 고민을 하는 사람에게 작은 팁으로나마 도움이 되길 바라며 예제 코드는 UYEONG/request-animation-frame-test에 올려놓았으니 참고하길 바란다.

  • 특정 목록 페이지에 접근할 때 사용자가 마지막으로 클릭했던 아이템이 보이도록 자동 스크롤링 해달라는 요청이 들어왔다. 개인적으로 onload 타임에 특정 목록으로 자동 스크롤링하는 기능은 지양해 왔기 때문에 웹이 가진 한계점을 설명하면서 간단히 프로토타이핑해본 후 판단하자고 의견을 냈다.

    그런데 프로토타이핑하던 도중 예전엔 경험하지 못했던 이상한 현상이 발견됐다. 아래는 사용자가 마지막으로 클릭한 아이템이 “product30” 이라고 보고 해당하는 엘리먼트의 위치로 스크롤링하는 간단한 코드다.

    window.addEventListener('load', () => {
        console.log('onloaded');
    
        const product30 = document.getElementsByClassName('product-item')[29];
        const top = product30.getBoundingClientRect().top;
    
        console.log(`scroll to ${top}(product30)`);
    
        window.scrollTo(0, top);
    });

    생각한 대로라면 지정한 위치로 스크롤링 되어야 하지만 동작하지 않았다. 아래 “그림 1”을 보면 분명히 9970.875 즉, “product30”의 위치로 스크롤링을 지시했음에도 문서는 여전히 최상단에 있음을 알 수 있다.

    동작하지 않는 scrollTo()
    <그림 1. 동작하지 않는 scrollTo()>

    간단한 코드로 재현하긴 힘들지만, 실제 서비스 페이지에서는 setTimeout(() => ..., 0)으로 지정해도 금세 원래의 위치로 크롬이 재보정한다.

    이렇게 동작하는 이유는 크롬이 사용자가 보고 있던 스크롤 위치를 저장하고 있다가 해당 페이지에 다시 접근하면 브라우저 레벨에서 자동으로 스크롤링하기 때문인데 어떻게 해야 이를 회피할 수 있을지 고민됐다. 이때 찬욱님에게 여쭤보니 아래와 같은 코드로 이를 무력화할 수 있다는 답변을 받았다.

    if ('scrollRestoration' in history) {
        // Back off, browser, I got this...
        history.scrollRestoration = 'manual';
    }

    위 코드를 삽입하고 다시 페이지에 접근하니 정확히 의도한 대로 동작했다.

    제대로 동작하는 scrollTo()
    <그림 2. 제대로 동작하는 scrollTo()>

    프로토타이핑은 무사히 완료했고 의사 결정하는데 큰 역할을 했다. 하지만 이렇게 이야기를 마무리하기엔 찜찜하다. scrollRestoration 속성을 좀 이해하고 넘어가야 할 것 같다.

    필자는 이번에 scrollRestoration이라는 속성을 처음 봤다. 그도 그럴 것이 scrollRestoration은 실험적(Experimental) API 에다가 아직 MDN에 페이지도 없고(참고), 2015년 10월에 배포된 크롬 46에서야 추가된 API다.

    scrollRestoration은 히스토리 네비게이션의 스크롤 복원 기능을 명시적으로 지정할 수 있는 속성이다. 속성값은 ‘auto’와 ‘manual’이 전부. SPA 환경과 관련이 있어 보인다. 이 글을 읽는 사람 중 대다수는 목록 페이지에서 특정 아이템을 클릭해 엔드 페이지로 갔다가 다시 되돌아오면 스크롤을 처음부터 다시 해야 하는 경험을 한 적 있을 것 같다. 그래서 스크롤 포지션 값을 LocalStorage에 저장했다가 다시 목록 페이지에 접근하면 억지로 스크롤 위치를 잡아주는 기능을 구현한다.

    대표적으로 네이버 주식 모바일 웹이 그런 방식으로 구현돼 있는데 토론 목록 페이지에서 토론 페이지로 접근하면 scrollY 값을 기억해 뒀다가, 뒤로 가기 하여 토론 목록 페이지로 되돌아오면 이 값을 이용해 스크롤 위치를 조절한다.

    보던 목록으로 스크롤링하기 위해 scrollY 값을 기억한다
    <그림 3. 사용자가 보던 곳으로 보정하기 위해 scrollY 값 저장>

    위와 같은 구현 방식은 사용자가 정확히 어느 경로를 통해 목록 페이지로 접근하는지 알기 어려워 자칫 잘못된 경험을 제공(검색을 통해 접근했는데 스크롤 위치를 조절하는 등)하기도 하는데 scrollRestoration은 history navigation을 기반으로 동작하기 때문에 이러한 부분을 해소할 수 있을 것으로 보인다. 하지만 처음에 이야기한 것처럼 자동 스크롤링 기능이 오히려 특정 기능 구현에 방해가 될 수도 있는데 세심하게 개발자에게 조절할 수 있도록 속성을 열어줘서 고마울 따름.

    테스트 코드는 UYEONG/scroll-restoration-test에 올려놓았으니 참고하길 바란다.

    참고

  • 인생을 살다 보면 괴롭지만 꼭 해야만 하는 일을 만나게 된다. 프런트엔드개발자에겐 그런 일 중 하나가 바로 UI 테스트가 아닐까 싶은데, 이 고통스러운 일을 조금이나마 덜어줄 잘 만들어진 도구나 프레임워크를 찾지만 쉽지 않다. 처음엔 좋아 보여도 실제 테스트를 작성하다 보면 금세 그 도구가 가진 한계점을 만나게 된다. 그래서 그런지 다른 일보다도 더욱 도구에 의존하게 되고 개선된 또 다른 도구를 찾게 되는 것 같다.

    TestCafe는 자바스크립트 소식을 매주 정리해 공유하는 사이트인 JSer.info2016년 10월 24일 자 소식을 통해 알게 됐다. 해당 문서에 링크된 TestCafe로 브라우저 자동 테스트(일본어)를 읽어보았는데 생각보다 느낌이 좋아서 한번 리뷰해보자는 결론을 내렸다.

    TestCafe 소개

    TestCafe는 DevExpress가 개발한 E2E 테스트 프레임워크다. InfoQ에 TestCafe와 관련된 인터뷰 글(
    TestCafe with Smart Script Injection, Traffic and Markup Analysis Tools
    )이 있으니 관심 있는 사람은 참고하길 바란다. 같은 이름의 웹 서비스 및 클라이언트 앱도 서비스 중인데 이 서비스는 셀레니움 IDE 처럼 GUI로 조작하고 행위를 기록하여 재생할 수 있다.

    TestCafe는 webdriber.io나이트왓치와는 다르게 테스트 관련 스크립트를 주입해 동작하는 셀레니움 RC(Selenium RC)와 흡사한 방식으로 개발됐다. 사실 셀레니움 RC가 가진 한계를 극복하고자 셀레니움 웹드라이버(Selenium WebDriver)를 개발했는데 다시 셀레니움 RC와 비슷한 구조로 테스트 프레임워크를 만들었다고 해서 “그렇다면 과거에 경험했던 한계를 그대로 답습하는 게 아닌가?”하고 조금 의아했다.

    TestCafe의 개발자 이반 니쿨린(Ivan Nikulin)Why not use Selenium?에서 그 이유를 밝혔는데 간단히 말해서 테스트 환경에 대한 복잡한 설정 없이 실행할 수 있고, 모바일 기기에서도 원격 접속해 테스트할 수 있는 도구를 만들고 싶어 했던 거 같다. 또, 웹드라이버의 호환성 문제를 회피하기 위한 목적도 있는 것 같다.

    셀레니움은 분명 훌륭한 도구지만 설정이 복잡하고 웹드라이버 자체의 버그로 인해 테스트 작성에 종종 걸림돌이 되는 경우가 있다. 또 테스트 코드 자체를 디버깅하기가 까다로워 복잡한 테스트 케이스를 작성하는데 어려운 면도 가지고 있다. 과거 셀레니움 RC 방식에 한계가 있어 셀레니움을 만들었지만 새로운 문제들이 나타났다. 이러한 상황에서 TestCafe의 지향점이 좋은 해결책이 될 수 있을까?

    좋은 인상

    필자는 유료 웹툰을 서비스하고 있는 레진(Lezhin)을 이용해 로그인 테스트를 작성해 봤다. 예제 코드는 저장소 UYEONG/demo-testcafe를 참고한다. 이번 절에서는 이 예제를 이용해 필자가 받은 몇 가지 좋은 인상을 소개하겠다.

    test('사용자는 GNB 메뉴에서 로그인할 수 있다.', async (t) => {
        // Given
        const popupAttendanceLogin = new PopupAttendanceLogin(t);
    
        if (await popupAttendanceLogin.exist()) {
            await popupAttendanceLogin.close();
        }
    
        await t
            .click('#main-menu-toggle')
            .typeText('#login-email', ACCOUNT.USER_NAME)
            .typeText('#login-password', ACCOUNT.PASSWORD);
    
        // When
        await t
            .click('form.login-form button[type=submit]')
            .wait(1000);
    
        // Then
        await t.click('#main-menu-toggle');
    
        const email = await getElement('sidenav-email');
    
        assert(email.visible);
        assert(email.innerText === ACCOUNT.USER_NAME);
    });

    위는 레진에서 GNB 메뉴를 이용해 로그인이 정상적으로 이뤄지는지 테스트하는 코드다. 그리고 이 코드는 await/async를 이용해 비동기적 절차를 동기적으로 표현하고 있다. TestCafe는 바벨(Babel)을 내장하고 있어 별도의 설정 없이 최신 사양을 이용할 수 있다. 최신 사양으로 코드를 작성하고자 할 때 복잡한 세팅을 해줘야 하는 기존의 테스트 프레임워크와는 다른 부분이다.

    TestCafe에서 디버깅하기
    <그림 1. TestCafe에서 디버깅하기>

    또, await/async 방식으로 테스트 코드를 작성하면 디버깅이 쉽다는 장점이 있는데 체이닝을 펼치기 쉬우므로 각 액션을 단계별로 관찰할 수 있다. 나이트왓치는 파이프라인 방식으로 디자인돼 있어 디버깅이 다소 까다롭다.

    그럼 이제 실행을 해보자. 해당 저장소를 클론하고 다음 명령어를 입력하면 바로 테스트할 수 있다.

    $ git clone git@github.com:UYEONG/demo-testcafe.git
    $ npm install
    $ npm run test

    npm scripts에 등록한 test 명령은 다음과 같다.

    $ testcafe chrome tests/

    뭔가 추가적인 설정이 없으니 오히려 불안하다. 하지만 그 안락함에 금방 익숙해진다. 이것저것 세팅해줘야 했던 셀레니움 기반 프레임워크(참고)와는 사뭇 다른 경험이다.

    로그인 테스트 실행 결과
    <그림 2. 로그인 테스트 실행 결과>

    그림 2는 로그인 테스트가 진행되는 모습이다. 이 테스트의 진행 절차는 다음과 같다.

    1. www.lezhin.com 페이지에 접근한다.
    2. 최초에 출력된 팝업이 있다면 닫는다.
    3. 우측 상단의 메뉴 버튼을 클릭한다.
    4. 이메일 / 패스워드를 입력하고 로그인 버튼을 선택한다.
    5. 페이지가 갱신되면 다시 우측 상단의 메뉴 버튼을 클릭한다.
    6. 로그인이 정상적으로 완료 됐는지 확인한다.

    특정 절차에서 다음 절차로 넘어가기 위해선 지연 시간(Delay time)이 필요하다. 예를 들어 최초 페이지에 접근할 때는 콘텐츠가 모두 출력되는 시점을 기다려야 하고 팝업을 닫을 때는 애니메이션(FadeOut)이 종료되는 시간을 기다려야 한다. 나이트왓치에서는 이런 지연 시간을 직접 명시해줘야 한다.

    // 페이지에 최초 접근 시 body 엘리먼트가 보일 때까지 5000ms 기다린다.
    this._header
        .navigate()
        .waitForElementVisible('body', 5000);
        
    // 팝업을 닫을때 애니메이션 시간을 고려해 500ms 기다린다.
    this.click('@close');
    this.api.pause(500);

    하지만 TestCafe를 이용할 땐 지연 시간을 직접 입력할 일이 상대적으로 적다. TestCafe는 지연 시간을 직접 계산하고 관리한다. 실제로 위 로그인 테스트 코드를 보면 지연 시간을 명시한 지점은 로그인 버튼을 클릭한 시점 즉, 폼을 서브밋하고 갱신되기를 기다리는 딱 한 곳뿐이다.

    await t
        .click('form.login-form button[type=submit]')
        .wait(1000);

    지연 시간이라고 해도 거의 대충 시간을 짐작해 입력하는 일에 불과하다. 물론 비기능적 요구사항도 테스트에 포함돼야 하지만 애니메이션 종료 시점까지 일일이 명시해야 한다는 것은 분명 귀찮은 일이다.

    그리고 이벤트 지점을 커서로 표현해주거나 실제 타이핑을 하는 느낌을 살려 텍스트를 입력하는 부분도 인상적이다. 셀레니움 기반 테스트 프레임워크는 이런 자연스러운 느낌이 상대적으로 적다.

    TestCafe의 에러 리포팅
    <그림 3. TestCafe의 에러 리포팅>

    마지막으로 에러 리포팅도 상당히 깔끔한 편인데 어느 지점에서 어떠한 에러가 낫는지 알기 쉽게 출력해준다. 그림 3을 보면 24번째 행의 코드에 문제가 있음을 쉽게 알 수 있다.

    구구절절 설명했지만 TestCafe를 리뷰하면서 좋은 인상을 받은 점을 간단히 정리하면 다음과 같다.

    • 바벨을 내장하고 있어서 특별한 설정 없이 ES6+ 사양을 사용할 수 있다.
    • 테스트 코드 디버깅이 상대적으로 쉽다.
    • 특별한 설정 없이 커멘드 라인 명령으로 바로 테스트할 수 있다.
    • 특정 조작에 대한 지연 시간을 자동으로 관리한다.
    • 테스트 실패 및 에러 리포팅이 깔끔한 편이다.

    아쉬운 점

    분명 기존의 E2E 테스트 프레임워크보다 몇 가지 좋은 인상을 가지고 있는건 분명하다. 하지만 아쉬운 점도 있다. 일단 다양한 상황을 테스트하기엔 액션 셋과 API가 부족하다.

    TestCafe와 그 외 프레임워크의 API 목록
    <그림 4. TestCafe와 그 외 프레임워크의 API 목록>

    왼쪽 부터 차례대로 TestCafe, 나이트왓치, webdriver.io 가 제공하고 있는 액션 및 API 목록이다. 기본적인 액션은 제공하지만, 모바일에 특화된 액션이나 스크립트 권한 밖의 액션 등은 이용하기 힘들다. 시간이 지나면서 제공될 수 있는 API도 있지만 TestCafe가 가지고 있는 구조적 한계로 인해 아예 불가능한 API도 있다.

    또, E2E 테스트를 할 때 좋은 패턴들이 있는데 그중 하나가 PageObject다. 테스트에 필요한 반복적인 행위나 엘리먼트 셀렉터 등을 밖으로 노출 시키지 않고 페이지 단위(혹은 컴포넌트 단위)로 추상화해 제공할 수 있다(참고). PageObject는 테스트 코드의 가독성이나 유지 보수 측면에서 훌륭한 패턴이지만 TestCafe에서는 제공하지 않는다.

    // page-objects/popup-attendance-login.js
    import {Selector} from 'testcafe';
     
    const querySelector = Selector(q => document.querySelector(q));
     
    class PopupAttendanceLogin {
        elements = {
            wrapper: '#popup-attendance-login',
            closeBtn: '#popup-attendance-login .attlogin__close'
        };
    
        constructor(testController) {
            this.t = testController;
        }
    
        async exist() {
            const wrapper = await querySelector(this.elements.wrapper);
            return wrapper.visible;
        }
    
        async close() {
            const closeBtn = await querySelector(this.elements.closeBtn);
            await this.t.click(closeBtn);
        }
    }
     
    export default PopupAttendanceLogin;
    
    // tests/signin-test.js
    import PopupAttendanceLogin from '../page-objects/popup-attendance-login';
    
    test('사용자는 GNB 메뉴에서 로그인할 수 있다.', async (t) => {
        // Given
        const popupAttendanceLogin = new PopupAttendanceLogin(t);
    
        if (await popupAttendanceLogin.exist()) {
            await popupAttendanceLogin.close();
        }
        // ... 생략 ...

    그래서 필자는 위 코드처럼 직접 PageObject와 비슷한 객체를 직접 만들고 테스트 코드를 작성했다. 만약 프레임워크 자체에서 이 개념을 제공한다면 조금 더 편리하게 코드를 작성할 수 있을 것 같다.

    끝으로

    여기까지 TestCafe를 소개하고 필자가 느낀 좋은 인상과 아쉬운 점을 함께 이야기했다. 이 도구가 우리의 UI 테스트 환경의 답이 돼 줄 것이라 생각하지 않는다. 분명 실제로 테스트를 작성하기 시작하면 온갖 버그와 미흡한 점을 만나게 될 것이다. 하지만 아직 시작된 지 얼마 안 된 프로젝트라는 점을 미루어 볼 때 차차 개선될 것이라고 긍정적으로 생각할 수 있다.

    중요한 건 그들이 어떤 문제를 해결하고 싶어 하고 어디에 지향점을 두고 있느냐다. 그것이 내 앞에 놓인 문제 혹은 환경과 맞아떨어진다면 더할 나위 없는 좋은 도구가 될 것이다.

  • 나는 소프트웨어 엔지니어다. 이 일로 돈을 벌고 가족을 책임지지만 여전히 엔지니어링 즉, 공학이란 무엇이냐는 단순한 질문에도 쉽게 대답하지 못한다. 나뿐만 아니다. 세상에는(적어도 내가 만난 엔지니어 중에는) 과학이나 수학의 세계를 공학과 혼동하는 사람도 있고 과학과 수학이 공학보다 더 우위에 있다고 말하는 사람도 있다. 모두 공학을 잘 이해하지 못해 일어나는 일이다.

    공학의 본질적 목표는 무엇이며, 엔지니어가 정확히 무슨 일을 하는 사람을 일컫는지 알지 못하면서 소프트웨어 공학 관련 직업에 종사하고 있다는 사실이 아이러니하게 느껴지기도 한다. 그래서 그런지 요샌 스킬-업보단 공학이란 무엇인가에 좀 더 관심을 두고 「공학 학교에서 배운 101가지」나 「맨발의 엔지니어들」 같은 조금은 덜 전문적이지만 딱딱하지 않은 책을 읽는데 시간을 내고 있다.

    이번에(사실 포스팅으로는 처음) 소개할 책은 「공학기술과 사회」다. 공학에 대한 여러 가지 잡생각을 하며 퇴근하던 중 강남 영풍문고에 들르게 됐는데, 진열장에 꽂혀 있는 이 책이 나의 눈길을 사로잡았다.

    공학 기술과 사회 책표지
    <그림 1. 공학 기술과 사회>

    책 표지나 내용 전개 방식을 보면 다소 대학 교재 같은 느낌이 난다. 실제로 한국공학교육센터에서 공학소양 교과목 DB구축사업의 목적으로 연구비를 받아 집필됐다고 한다. 교재 같은 점만 제외하면 책 내용 자체는 제법 훌륭한 편이다.

    엔지니어가 전공을 통하여 사회에 공헌하기 위해서는 자기가 하는 일이 사회와 어떤 관계가 있으며, 기술이 전반적으로 사회와 서로 어떤 영향을 주고 받는지 이해할 필요가 있다.

    소프트웨어 엔지니어는 현재에 집중하여 좀 더 나은 미래를 만들기 위해 살아가지만, 사회에 있어 의미 있는 무엇인가를 만들기 위해선 사회와 기술의 관계를 이해할 필요가 있다. 눈앞에 놓여있는 기술적 문제 해결에만 집중하기보단 조금 더 넓은 시야를 가지고 다양한 사회적 문제를 어떻게 해결할 수 있을지, 사회 속에 엔지니어와 기술의 책임은 무엇일지 고민해야만 엔지니어로서 가장 만족할 수 있는 성과를 달성할 수 있지 않을까.

    이때 이 책이 그러한 시작점으로써 좋은 지적 재료가 될 수 있다. 공학의 정의부터 엔지니어적 가치, 기술의 역사, 기술결정론이나 기술 사회적 구성론 같은 기술사회론도 일반인이 이해하고 받아드리기 쉽게 작성돼 있다.

    앞으로도 공학의 본질적인 측면과 사회와의 관계를 이해하고 엔지니어로서의 책임 범위와 중요한 덕목을 갖춘 그런 훌륭한 엔지니어가 되기 위해 끊임없이 정진해야 할 것 같다. 먼 훗날 한 사람의 엔지니어로서 훌륭한 인생을 살았다고 스스로 자신 있게 말할 수 있도록 말이다.

    그나저나 모두 베이컨을 아는가? 먹는 베이컨이 아니라 “아는 것이 힘이다”라고 말한 철학자 프랜시스 베이컨(Francis Bacon) 말이다(최근에 FRP 라이브러리로도 등장했다 -Bacon.js-). 이 책을 통해서 알게 된 사실인데 베이컨은 공학적 측면에서도 상당히 멋진 분인 걸 알 수 있었다.

    근데 초 서양에서 기술의 가치를 높게 평가하고 기술 진보를 인류의 역사에서 매우 중요한 과제로 제시함으로써 플라톤과 아리스토텔레스 이후 지속돼왔던 “기술에 대한 천대”를 종식시키려 노력했던 대표적 인물이 바로 프랜시스 베이컨(Francis Bacon)이다. … 중략 … 베이컨은 발명을 통한 기술적 진보가 사회 발전에 필수적인 요소임을 강조함으로서 과학은 고상하지만 기술은 비천하다는 편견을 없애려 했다.

    우리 모두 자부심을 가지고 유용한 제품을 만들어내는 훌륭한 공학자의 인생을 살아나가자. 기술은 절대 천하지 않고 공학은 위대하다! 끝.

  • 나이트왓치를 소개하기 전에 E2E 테스트의 정의부터 셀레니움 웹드라이버 등 기본 개념부터 간단히 소개하겠다.

    E2E 테스트

    정의

    소프트웨어 테스트는 테스트의 규모(레벨)에 따라 유닛 테스트, 통합 테스트, 시스템 테스트, 인수 테스트 이렇게 4가지로 분류한다. 여기에서 E2E 테스트는 시스템 테스트에 속한다.

    E2E(End-to-End) 테스트는 전체 시스템이 제대로 작동하는지 확인 하기 위한 테스트로 시나리오 테스트, 기능 테스트, 통합 테스트, GUI 테스트를 하는데 사용한다. API와의 연동도 테스트 항목에 포함되기 때문에 일반적으로 목(Mock)이나 스텁(Stub)과 같은 테스트 더블을 사용하지 않으며 최대한 실제 시스템을 사용하는 사용자 관점에서 시뮬레이션 한다. 그래서 테스트 속도가 서비스 규모에 따라 상당히 느릴 수 있기 때문에 유닛 테스트나 기능 테스트를 위한 일반적인 테스트 자동화와 시스템 테스트를 위한 E2E 테스트 자동화를 함께 구성한다.

    E2E 테스트 프레임워크

    E2E 테스트 프레임워크는 다양한 종류가 있는데, 크게 헤드리스 브라우저를 의존하는 것과 셀레니움 웹드라이버를 의존하는 것으로 나눌 수 있다. 셀레니움 웹드라이버는 다음 절에서 자세히 설명한다.

    헤드리스 브라우저는 커맨드 라인 명령어로 조작할 수 있는 화면이 없는 브라우저로 Jsdom 기반의 좀비(Zombie.js), 웹킷 엔진 기반의 팬텀(Pantom.js), 겟코 엔진 기반의 슬리머(Slimer.js) 등이 있다. 잘 알려진 캐스퍼(Casper.js)는 팬텀과 슬리머를 조금 더 사용하기 쉽게 만들어 놓은 유틸리티 도구다. 헤드리스 브라우저는 기본적으로 크로스 브라우징 테스트가 불가능하며 어썰트(Assert)도 내장하고 있지 않기 때문에 필요하다면 추가를 해야한다.

    셀레니움 웹드라이버를 의존하는 프레임워크로는 webdriver.io, 큐컴버(Cucumber.js), 프로트랙터, 나이트왓치 등이 있다. 이들은 크로스 브라우징 테스트가 가능하고 어썰트도 내장하고 있다. 단, 각 프레임워크 마다 내장하고 있는 어썰트 라이브러리는 다르다.

    셀레니움 웹드라이버

    나이트왓치는 셀레니움 웹드라이버(Selenium WebDriver) API를 사용해 개발된 E2E 테스트 프레임워크이기 때문에 본격적으로 사용해보기 전에 셀레니움과 웹드라이버를 먼저 이해할 필요가 있다.

    셀레니움 웹드라이버의 원래 이름은 셀레니움(또는 셀레니움 1.0)이었다. 셀레니움은 웹 브라우저를 사용하여 웹 애플리케이션을 테스트하는 오픈 소스 도구다. 이때 사람의 손으로 직접 웹 브라우저를 조작하는 것이 아니라 작성된 스크립트에 따라 자동으로 조작한다. 이러한 방법을 브라우저 자동화(Browser Automation)라고 표현한다.

    셀레니움은 시카고에 위치한 소트워크스(ThoughtWorks) 사에서 개발을 시작했다. 소트워크스는 마틴 파울러(Martin Fowler)가 속한 그룹으로 유명하다.

    웹드라이버는 셀레니움의 단점을 보완하고자 구글의 엔지니어들이 개발하고 사용한 브라우저 자동 테스트 도구이다. 2006년 경 구글에서 근무 중이던 시몬 스튜어트(Simon Stewart)가 주도해 프로젝트를 시작하고 2009년에 처음으로 공식 발표했다.

    Selenium Projects
    <그림 1. 셀레니움 프로젝트의 흐름>

    과거 셀레니움은 자체 엔진인 셀레니움 RC(Remote Control)를 이용해 브라우저와 통신했다.

    셀레니움 RC는 자바나 파이썬 등의 언어로 스크립트를 작성하면 그 스크립트를 기반으로 브라우저를 조작하는 자바스크립트를 생성하고 해당 페이지에 삽입 후 브라우저를 조작하는 간단한 구조였다. 이러한 구조는 브라우저의 보안 제약이나 자바스크립트의 한계로 인해 실효성이 떨어지는 단점이 있었다. 이 단점이 시몬 스튜어트가 웹드라이버를 만들게 된 이유이기도 하다.

    그에 반해 웹드라이버는 브라우저의 확장 기능과 OS의 기본 기능 등을 이용하여 브라우저를 조작하는 구조였다. 이는 셀레니움 RC의 단점을 충족해줄 수 있는 방식이었다.

    Selenium webdriver high level block diagram
    <그림 1. 셀레니움 웹드라이버 다이어그램>

    이 방식이 성공하여 셀레니움 RC와 웹드라이버 통합이 이루어졌고 2011년 7월에 셀레니움 웹드라이버(또는 셀레니움 2.0)를 릴리즈하게 된다. 즉, 현재 우리가 알고있는 셀레니움은 웹드라이버와 통합한 버전이다.

    그림 2를 보면 알 수 있듯이 웹드라이버는 다양한 브라우저와 환경을 대응해야하는데, 브라우저마다 이를 위한 API가 다를 경우 또 다른 문제가 발생할 수 있기 때문에 현재 표준화를 제정(W3C WebDriver) 중이다.

    현재 셀레니움 웹드라이버는 파이썬, 루비, 자바, C## 그리고 Node.js를 이용해 웹브라우저는 조작할 수 있도록 다양한 API를 제공하고 있다. 하지만 셀레니움 서버와 자바스크립트의 궁합이 좋지 않고, 돔을 조작 하거나 셀렉팅하는데 한계가 있어 셀레니움 웹드라이버와 노드를 바인딩하여 다양한 기능을 제공하는 여러가지 형태의 프로젝트가 생겨났다. 그 중 유명한 프로젝트가 바로 webdriver.io와 나이트왓치 그리고 앵귤러 프로젝트를 위한 프로트랙터다.

    이들 도구는 웹드라이버 API를 사용할 때 생기는 다양한 패턴을 추상화한 API와 신택스 슈가 등을 제공해 셀레니움 2.0 보더 더 편리하고 다양한 경험을 제공한다.

    나이트왓치

    나이트왓치는 노드 기반의 E2E 테스트 프레임워크다. 셀레니움 웹드라이버를 중개하여 각종 브라우저를 조작하고 동작이 기대한 것과 일치하는지 테스트하는데 사용한다. CSS 셀렉터로 엘리먼트를 셀렉팅하여 테스트를 작성할 수 있도록 하는 기능과 신텍스 슈가 그리고 단순하고 간결한 문법을 제공한다. 또한 테스트 러너를 포함하고 있으므로 독자적으로 그룹화한 테스트를 한번에 실행할 수 있으며 지속적인 통합의 파이프 라인과 합칠 수 있다는 특징을 가지고 있다.

    나이트왓치를 알게 된건 나보다 먼저 E2E 테스트를 리서치하고 관련 도구를 찾고있던 훈민이형(개발왕 김코딩, 블로그) 덕분이었다. 미리 삽질을 하고 계셨기 때문에 다른 도구를 선택하기 보다 같이 삽질하는 편이 고민할 시간도 적어서 큰 고민 없이 사용했다.

    설치하기

    나이트왓치 설치는 개발자 가이드 Getting Started 절에 잘 설명돼 있다. 이 문서에는 간단하게 요약해 설치 과정을 설명한다. 우선 NPM을 이용해 설치한다.

    $ npm install --save-dev nightwatch

    웹드라이버로 브라우저와 통신하기 위해서는 셀레니움 서버를 실행시켜야한다. 셀레니움 서버 다운로드 사이트에서 파일을 다운 받고 아래와 같이 서버를 실행한다. 이 글을 작성하는 현재 기준 가장 최신 버전은 2.53.0 이다.

    프로젝트 디렉터리에서 nightwatch.json을 생성하고 다음과 같이 작성한다. 옵션의 자세한 설명은 개발자 가이드 Configuration 절을 참고한다.

    {
      "src_folders" : ["tests"], // 테스트할 디렉터리, 배열로 지정
      "output_folder" : "tests/reports", // JUnit XML 리포트 파일이 저장될 위치
      "custom_commands_path" : "", // 불러올 커스텀 커맨드가 있는 위치
      "custom_assertions_path" : "", // 불러올 커스텀 어썰트가 있는 위치
      "page_objects_path" : "", // 불러올 페이지 객체가 있는 위치
      "globals_path" : "", // 불러올 외부 글로벌 모듈이 있는 위치
      "selenium" : {   // 셀레니움 서버 환경 설정
        "start_process" : true, // 테스트 시작시 셀레니움 프로세스를 자동으로 실행할 것 인지 여부
        "server_path" : "./selenium-server-standalone-2.53.0.jar", // 셀레니움 서버 jar 파일의 경로, start_process가 false면 지정하지 않아도 된다.
        "log_path" : "tests/logs", // 셀레니움의 output.log 파일이 저장될 경로
        "host" : "127.0.0.1", // 셀레니움 서버의 listen ip
        "port" : 4444, // 셀레니움 서버의 listen port
        "cli_args" : { // 셀레니움 프로세스로 넘겨질 cli 인자 목록
          "webdriver.chrome.driver" : "",
          "webdriver.ie.driver" : ""
        }
      },
      "test_settings" : { // 테스트 브라우저 별 환경 설정
        "default" : { // 모든 브라우저에 적용 될 공통 설정
          "launch_url" : "http://localhost",
          "selenium_port"  : 4444,
          "selenium_host"  : "localhost",
          "silent": true, // 셀레니움의 로그를 숨길지 여부
          "screenshots": { // 테스트가 실패 했을 때 촬영 될 스크린샷 설정
            "enabled" : true,
            "on_failure" : true,
            "on_error" : false,
            "path" : "tests/screenshots"
          },
          "desiredCapabilities": { // 셀레니움 웹드라이버로 전달할 브라우저 이름과 기능 지정
            "browserName": "firefox",
            "javascriptEnabled": true,
            "acceptSslCerts": true
          }
        }
      }
    }

    tests 디렉터리 하위에 demo.js를 생성하고 간단한 테스트 코드를 한다.

    module.exports = {
        '사용자는 검색어를 입력 후 검색어가 포함된 자동 완성 리스트를 볼 수 있다.' : function (browser) {
            browser
                .url('http://www.google.com')
                .waitForElementVisible('body', 1000)
                .setValue('input[type=text]', 'nightwatch')
                .pause(1000)
                .assert.containsText('##sbtc', 'nightwatch')
                .end();
        }
    };

    이어서 아래 명령어로 간단한 E2E 테스트를 실행할 수 있다.

    $ ./node_modules/nightwatch/bin/nightwatch

    하지만 현재 파이어폭스 버전 47에 문제가 있어 테스트가 실행되지 않을것이다. 파이어폭스에서 테스트 하고 싶다면 예전 버전(Install an older version of Firefox)으로 다운그레이드 하거나 GeckoDriver를 사용해야한다(Setting up the Marionette executable). 여기에서는 GeckoDriver를 이용하는 방법을 소개(OSX 기준)하겠다.

    먼저 mozilla/geckodriver에서 GeckoDriver를 다운로드한다.

    $ cd ~/Downloads
    $ wget https://github.com/mozilla/geckodriver/releases/download/v0.8.0/geckodriver-0.8.0-OSX.gz

    다운로드한 파일을 압축 해제하고 적당한 위치로 옮긴 후 실행가능한 파일로 변경한다.

    $ gunzip geckodriver-0.8.0-OSX.gz
    $ mkdir executable && mv geckodriver-0.8.0-OSX executable/wires
    $ chmod 755 executable/wires

    이제 .bash_profile(또는 .zshrc)에서 PATH를 지정한다.

    $ vim ~/.zshrc
    
    	GECKO_DRIVER=$HOME/Downloads/executable
    	export PATH=$HOME/bin:/usr/local/bin:/usr/local/sbin:$GECKO_DRIVER:$PATH
    
    ## rc파일을 다시 불러온다.
    $ source ~/.zshrc

    마지막으로 nightwatch.json파일에서 desiredCapabilities 속성을 다음과 같이 변경한다.

    "desiredCapabilities": {
      "browserName": "firefox",
      "marionette": true, // 추가
      "javascriptEnabled": true,
      "acceptSslCerts": true
    }

    이제 다시 실행해보면 파이어폭스 브라우저에서 정상적으로 테스트가 진행될 것이다.

    데모 테스트 실행 결과
    <그림 3. 데모 테스트 실행 결과>

    여러 브라우저에서 동시에 테스트하기

    현재 작성한 설정 파일로 나이트왓치를 실행하면 파이어폭스에서만 테스트가 진행된다. 이번엔 크롬 브라우저에서도 테스트가 진행되도록 설정을 변경하겠다. 크롬 브라우저는 셀레니움과 통신할 웹드라이버를 별도로 설치해야하는데 웹드라이버 매니저를 사용하면 쉽게 설치할 수 있다. 아래 명령어로 웹드라이버 매니저를 설치한다.

    $ npm install --save-dev webdriver-manager
     
    # 크롬 웹드라이버와 앞 절에서 다운로드 받았던 셀레니움 서버가 함께 설치된다.
    $ ./node_modules/.bin/webdriver-manager update
     
    # 또는 아래 명령어 처럼 인자를 전달해 별도로 설치할 수도 있다.
    # ./node_modules/.bin/webdriver-manager update --chrome

    이제 nightwatch.json에 셀레니움 서버 경로와 크롬 웹드라이버 서버 경로를 수정한다.

    "selenium": {
      "start_process": true,
      "server_path": "./selenium-server-standalone-2.53.0.jar",
      "log_path": "tests/logs",
      "host": "127.0.0.1",
      "port": 4444,
      "cli_args": {
        "webdriver.chrome.driver" : "node_modules/webdriver-manager/selenium/chromedriver_2.21", // 추가
        "webdriver.ie.driver": ""
      }
    },

    다음으로 default 속성에 작성했던 파이어폭스 브라우저 설정을 test_settings 속성 하위로 옮기고 크롬 브라우저 설정도 함께 추가 작성한다.

    {
      // ... 생략 ...
      "test_settings": {
        "default": {
          "launch_url": "http://localhost",
          "selenium_port": 4444,
          "selenium_host": "localhost",
          "silent": true,
          "screenshots": {
            "enabled" : true,
            "on_failure" : true,
            "on_error" : false,
            "path" : "tests/screenshots"
          }
        },
        "firefox": {
          "desiredCapabilities": {
            "browserName": "firefox",
            "marionette": true,
            "javascriptEnabled": true,
            "acceptSslCerts": true
          }
        },
        "chrome": {
          "desiredCapabilities": {
            "browserName": "chrome",
            "javascriptEnabled": true,
            "acceptSslCerts": true
          }
        }
      }
    }

    이제 아래 명령어로 실행하면 두 브라우저에서 동시에 테스트가 실행된다.

    $ ./node_modules/nightwatch/bin/nightwatch --env firefox,chrome

    사파리 브라우저에서 테스트하고자 한다면 사파리 웹드라이버를 확장 기능으로 설치해야한다. 자세한 내용은 나이트왓치 위치의 Running tests in Safari 문서를 참고한다.

    사파리의 웹드라이버 확장프로그램
    <그림 4. 사파리의 웹드라이버 확장프로그램>

    모카 사용하기

    이번엔 테스트 코드를 모카 기반으로 작성할 수 있는 환경을 만들어보겠다. 나이트왓치는 어썰트로 챠이(chai)를 내장하고 있지만 모카는 별도로 설정해 사용해야한다. 모카를 설정하는 자세한 내용은 개발자 가이드 Using Mocha 절을 참고한다. 모카를 굳이 사용하려는 이유는 JUnit XML로 리포팅 하는 기본 러너와는 달리 다양하고 보기 쉬운 리포팅을 지원하기 때문이다.

    먼저 nightwatch.json 파일에 다음과 같이 test_runner 속성을 추가한다. 옵션에 관한 자세한 설명은 모카 위키의 Set options 절을 참고한다.

    {
      "test_runner" : {
        "type" : "mocha",
        "options" : {
          "ui": "bdd",
          "reporter": "spec"
        }
      },
      // ... 생략 ...
    }

    테스트 코드를 모카 기반으로 재작성한다.

    describe('구글 메인 페이지', function() {
     
        before(function(client, done) {
            done();
        });
     
        after(function(client, done) {
            done();
        });
     
        describe('##사용자는 검색할 수 있다.', function() {
            it('사용자는 검색어를 입력 후 자동 완성된 리스트를 볼 수 있다.', function(client, done) {
                client
                    .url('http://www.google.com')
                    .waitForElementVisible('body', 1000)
                    .setValue('input[type=text]', 'nightwatch')
                    .pause(1000)
                    .assert.containsText('##sbtc', 'nightwatch')
                    .end(done);
            });
        });
    });

    다시 실행해 보면 모카 기반으로 테스트 코드가 동작하는 것을 볼 수 있다.

    모카 테스트 실행 결과
    <그림 5. 모카 테스트 실행 결과>

    브라우저 스택

    크로스 브라우징 테스트를 할 수 있도록 해주는 웹 서비스인 브라우저 스택은 다양한 플랫폼과 웹 브라우저를 지원한다. 또한, 셀레니움 서버도 제공하고 있는데 이를 이용하면 나이트왓치와 연동해 테스트를 자동화할 수 있다.

    먼저 browserstack.json 파일을 작성한다.

    {
      // ... 생략 ...
      "selenium": {
        "start_process": false
      },
      "test_settings": {
        "default" : {
          "launch_url" : "http://hub.browserstack.com",
          "selenium_host" : "hub.browserstack.com",
          "selenium_port" : 80,
          "silent" : true,
          "screenshots" : {
            "enabled" : true,
            "on_failure" : true,
            "on_error" : false,
            "path" : "tests/screenshots"
          },
          "desiredCapabilities": {
            "platform": "xp",
            "browserName": "firefox",
            "javascriptEnabled": true,
            "acceptSslCerts": true,
            "browserstack.user" : "user_id", // 브라우저 스택 아이디
            "browserstack.key" : "user_key" // 브라우저 스택 키
          }
        }
      }
    }

    platform 속성엔 XP를 browserName 속성엔 파이어폭스를 지정했고 로컬 환경에서 셀레니움 서버를 실행시킬 필요가 없기 때문에 start_process은 false로 지정했다. 이제 브라우저 스택은 윈도우즈 XP 환경의 파이어폭스 브라우저에서 테스트를 진행할 것이다. 브라우저 스택에서 지원하는 플랫폼과 브라우저는 공식 홈페이지의 Capabilities 페이지를 참고하면 알 수 있다.

    아래 명령어를 참고해 실행해본다.

    $ ./node_modules/nightwatch/bin/nightwatch --config browserstack.json

    다양한 플랫폼과 브라우저에서 E2E 테스트를 할 수 있다는 점은 큰 장점이지만 통신이나 테스트를 구동하는 속도가 아주 느리다. 따라서 테스트 배치 혹은 정기 배포 전에만 사용하기 적합해 보인다.

    웹스톰 디버깅

    웹스톰에서 노드 디버깅 도구를 사용해 나이트왓치를 디버깅할 수 있다. 자세한 내용은 Debugging Nightwatch tests in WebStorm을 참고한다. 다만, 파이프라인 방식이다 보니 브레이크 포인트를 활용한 디버깅이 다소 무의미한 느낌은 있다.

    끝으로

    여기까지 다양한 사전 지식을 설명하고 나이트왓치에 관해서 이해해봤다. E2E 테스트 특성 상 프로젝트 저장소에 테스트를 작성하고 유지하기 보단 별도의 E2E 테스트 저장소를 만들어 테스트를 작성하고 유지하는게 더 효율적이지 않을까 생각한다. 또, 나이트왓치에는 페이지 오브젝트, 커스텀 커맨드 등 테스트를 작성할 때 유용한 개념을 제공한다. 이 두 개념을 적절히 잘 사용하면 생각보다 더 관리하기 쉬운 테스트 코드를 작성할 수 있다.

    위에서 진행한 설치 및 설정 과정은 UYEONG/hello-nightwatch에 올려놓았으니 참고하길 바란다.

    참고

  • React에 대한 여러 가지 생각」이라는 글에 직접 코멘트를 달까 생각했지만, 쓰다 보니 글이 길어져서 포스팅한다. 해당 글과는 다른 개인적인 의견에 평소 이야기하고 싶었던 내용을 약간 첨부해 글을 작성했다.

    개인적으로 느꼈던 리액트의 가장 큰 장점은 사고의 단순함을 끌어내는데 있다고 생각한다(물론 애플리케이션의 성격이나 상황에 따라 다르다). 성능는 부차적인 것으로 앵귤러1 보다 빠른 메커니즘을 제공하지만, 당연 순수 자바스크립트보단 느리다.

    보통 자바스크립트 개발 시에는 일일이 변경을 검사해 해당하는 DOM을 가져와서 값을 대입해줘야 하는 번거로움(혹은 고통스러운)이 있다. 그래서 몇십 밀리세컨드는 신경쓰지 않고 처음부터 다시 그리는 방법을 택한 적도 있었지만 그것만으로 만족하긴 찜찜하다.

    HTTP의 Stateless 성을 기반으로 개발된 웹 MVC 프레임워크를 사용할 때는 요청이 들어오면 요청에 맞는 HTML을 생성해 응답해주면 끝나는 단순한 구조이기 때문인데 이런 고통스러운 부분이 적었다. 이때의 단순함을 리액트로 개발할 때 느꼈다.

    Stateless 하니 왠지 functional이라는 키워드도 떠오르는데 실제로 리액트에서 뷰는 어떤 값에 의해 생성되는 단순한 결과값(스냅샷)에 불과하다. 리액트의 가장 큰 장점은 여기에 있다고 생각한다. 이 부분이 불변, 단방향 데이터 플로우와 좋은 궁합을 보여주는 점이다. 실제로 React의 개발자인 Jordan Walke는 XHP와 함수형에서 영감을 받아 리액트를 개발했다(참고). 위와 같이 단순한 구조는 함수형에서, JSX는 XHP에서 영감을 받은 듯하다.

    JSX는 서술적으로 컴포넌트를 표현하는 데 좋은 표기법이라고 생각한다. UI의 구조를 표현하거나 각 컴포넌트를 조합하는 데는 명령형(Imperative)보다 선언형(Declarative)이 더 적합한 경우가 많다.

    아래는 jQuery로 뷰 로직을 작성할 때 자주 보이는 형태다.

    아래는 JSX로 표현했을 때의 모습이다.

    개인적으로 느꼈던 JSX의 단점은 애니메이션 표현에 있다. UI의 구조를 표현하기엔 적합하지만, 애니메이션처럼 뭔가 동적인 효과를 표현하기엔 오히려 장황하고 정확히 어떤 요소에 애니메이션을 적용하는지 한 번에 파악하기 힘들었다.

    <Animation fadeOut={true}>
    	<Something>
    		<Item/>
    		<Item/>
    	</Something>
    </Animation>
    
    // or
    
    $('.somthing').fadeOut();

    JSX의 또 다른 문제는 낯섦이다. 기존 템플릿 방식이 아닌 XML 스러운 표기법을 그대로 자바스크립트 내에 작성한다. 이러한 방식은 처음에 상당히 혼란스럽게 느껴질 수 있다. 여기에서 거부감을 느끼고, 싫어야해야만 하는 또 다른 이유를 찾아 나서는 경우도 있다. 낯섦이라는 거부감을 잠시 잊어야하는데 말 처럼 쉬운 일은 아니다. 이런 거부감은 나 역시 있었고, 눈에 익는데 시간이 걸렸다(원래 새로운 패러다임은 이해하고 받아들이기 힘든 법이다).

    해당 글에선 <hr>이나 <input>과 같은 한 줄 요소(Single-line element)도 닫아줘야 하는 이유를 HTML 규칙을 구현하는 데 한계가 있기 때문이라 말했지만, JSX는 XML-Like 한 언어이기 때문에 HTML이라기보단 XHTML에 가깝다. 그렇게 보면 당연한 부분이라고 생각한다.

    공식 홈페이지의 JSX 소개 글
    <그림 1. 공식 홈페이지의 JSX 소개 글>

    또한, 여러 문맥에 걸쳐 가상 돔의 속도를 언급한다. 가상 돔의 속도를 inferno와 같이 개선하는 방법도 있었는데 흥미로웠다. 랜더링 시 해당 DOM이 정적 요소인지 동적 요소인지 판단해 정적 요소라면 Diff 단계에서 아예 빼버린다. 이처럼 앞으로도 가상 돔의 속도를 개선할 수 있는 여지가 충분히 남아있다고 판단할 수 있다.

    네이버의 효과툰 뷰 같이 인터렉션이나 애니메이션이 복잡해 성능을 많이 신경 써야 하는 부분이라면 React의 대체재를 찾기보단 해당 부분만 순수 자바스크립트로 구현하고 react-hightchart 처럼 리액트가 읽을 수 있도록 어댑터만 제공하는 게 낫다.

    React Adapter
    <그림 2. React Adapter 다이어그램>

    하나의 라이브러리만으로 서비스 개발 전체를 보완하긴 힘들다. 리액트가 서비스 개발 전체에 정답이 돼 줄 것으로 생각해선 안된다. 그렇다고 특정 부분에 한계가 있다고 다시 전체를 보완할 만한 프레임워크를 찾는 것도 무리다.

    React의 본질은 성능이 아니다. 장점은 개인마다 느끼는 바가 다르겠지만, 개인적으로는 성능이라는 것에 너무 집중할 필요는 없다고 생각한다. React의 철학이 무엇이고 어떤 고통을 해결해주며 또, 어떤 고통은 해결해주지 못하는지 잘 이해하여 프로젝트 성격과 팀의 역량 등을 고려해 시기적절하게 사용하면 된다.

  • 이번에는 불변객체의 개념과 React에 그 개념을 적용했을 때 어떤 이점을 얻을 수 있는지 소개하고자 합니다.

    불변객체란?

    객체 지향 프로그래밍에 있어서 불변객체(Immutable object)는 생성 후 그 상태를 변경할 수 없는 객체를 말합니다. 불변객체의 반대말은 가변객체로 자바스크립트의 배열과 같이 객체 내에서 관리하는 값이나 상태를 변경할 수 있는 것을 말합니다.

    var greeting = new String('Hello World!!');
    
    greeting.replace('World', 'Gil-dong');
    greeting.valueOf(); // Hello World!!

    위 예에서 greeting 변수에 문자열 객체를 생성해 대입했습니다. 그리고 문자열 객체의 replace 메서드를 이용해 ‘World’라는 문자열을 ‘Gil-dong’으로 변경했습니다. 하지만 여전히 greeting의 값은 ‘Hello World’ 입니다.

    greeting에 생성한 문자열 객체는 불변 객체이므로 객체 자신이 소유하거나 관리하는 값 또는 상태를 바꿀 수 없습니다. 따라서 replace 메서드는 새로운 상태를 가지는 또 다른 객체를 생성합니다.

    변수에 값을 바꾸기 위해서는 아래 처럼 새로운 객체를 변수에 대입해야 합니다.

    var greeting = new String('Hello World!!');
    
    greeting = greeting.replace('World', 'Gil-dong');
    greeting.valueOf(); // Hello Gil-dong!!

    값 객체

    이러한 불변 객체의 특성은 우리가 밀접히 사용하는 Number, String, Boolean과 같은 값 객체에서 만날 수 있습니다. 값 객체란 비교 연산 시 자신의 상태보다 값(value)을 우선하는 단순한 객체를 말합니다.

    자바스크립트에서 비교 연산

    여기에서는 이해를 돕기 위해 생성자를 이용해 문자열이나 정수를 생성하고 있지만, 자바스크립트에서 생성자를 이용해 원시 타입 객체를 생성하면 비교 연산 시 참조를 이용해 비교합니다. 따라서 항상 리터럴 표기법으로 값을 다루기 바랍니다.

    
        new String('Hello') === new String('Hello'); // false
        new Number(5) === new Number(5); // false
    

    값 객체는 값을 이용해 새로운 값을 만들어 낼 수 있지만 값 자체를 변경할 수 없습니다. 즉, 불변입니다.

    var num = new Number(2);
    num = num + new Number(3);
    
    num.valueOf(); // 5

    위에서 숫자 2를 생성한 후 숫자 3을 더해 숫자 5를 얻고 있습니다. 숫자 2에 숫자 3을 더하는 것은 값 자체를 바꾸는 것이 아니라 새로운 값을 생성하는 것입니다. 이러한 특징은 상태를 변화시키지 않으며 새로운 값을 생성하는 함수형 스타일(functional style)과 닮았습니다.

    React.js와 불변객체

    React 컴포넌트의 라이프 사이클 메서드 중에는 shouldComponentUpdate 메서드가 있습니다. 이 메서드는 컴포넌트가 다시 그려지기 전에 호출되며 만약 false를 반환하면 컴포넌트의 VirtualDOM을 비교하지 않습니다.

    다량으로 엘리먼트를 출력하는 리스트나 피드와 같은 컴포넌트는 매번 VirtualDOM을 비교하게 되면 성능 문제가 발생할 수 있으므로 필수로 사용해야 하는 메서드입니다(대도록이면 모든 컴포넌트에 작성하는 습관을 들이는게 좋습니다).

    가변 객체일 때

    잘 알려진 TodoMVC를 예를 들어 설명하겠습니다.

    // todoItem.js
    shouldComponentUpdate(nextProps, nextState) {
      return (
        nextProps.todo !== this.props.todo ||
        nextState.label !== this.state.label
      );
    }

    todoItem 컴포넌트의 shouldComponentUpdate 메서드는 prop 속성으로 전달된 todo 객체를 비교하여 VirtualDOM을 비교할지 말지 결정하고 있습니다.

    // todoHome.js
    onUpdate(todoId, label) {
      this.todos.update(todoId, label);
    }
    
    // todos.js
    update(todoId, label) {
        var todo = this._todos.find((todo) => todo.id === todoId);
    
        todo.update(label);
        this.emit('update');
    }

    특정 todo의 label 값을 변경하라고 todos 모델 객체에 요청하고 있습니다. todos 모델 객체는 자신이 관리하는 todo 객체들 중 하나를 찾아서 값을 변경하고 변경 사실을 통지합니다. 하지만 todoItem 컴포넌트의 단순한 비교문으로는 todo 객체의 값이 변경됐는지 알 수 없습니다.

    todos 모델 객체에서 관리하는 todo 객체와 prop 속성으로 전달된 todo 객체의 참조가 동일하기 때문에 항상 참이되므로 의도한 결과를 얻을 수 없는 것입니다.

    // todoItem.js
    shouldComponentUpdate(nextProps, nextState) {
      return (
        nextProps.todo.label() !== this.props.todo.label() ||
        nextProps.todo.completed() !== this.props.todo.completed() ||
        nextState.label !== this.state.label
      );
    }

    shouldComponentUpdate 메서드의 비교문을 변경했습니다. 조금 복잡해졌습니다. 만약 하나의 객체에서 관리하고 있는 상태가 많을수록 이 비교문은 아주 복잡해질 것입니다.

    하지만 여전히 이 코드는 동작하지 않습니다. todos 모델 객체에서 특정 todo 객체의 상태를 변경하면 같은 todo 객체를 참조하는 todoItem 컴포넌트에도 동일하게 반영돼 상태가 변경됐는지 알 수 없습니다. 이처럼 가변 객체의 참조를 가지고 있는 어떤 장소에서 객체를 변경하면 참조를 공유하는 모든 장소에서 그 영향을 받기 때문에 객체를 참조로 다루기란 쉽지 않습니다.

    // todoHome.js
    render() {
        var todos = this.props.todos.forEach((todo) => {
            return <TodoItem key={todo.get('id')} todo={todo.clone()} />;
        });
    
        return (
            <ul>{todos}</ul>
        );
    }

    이번엔 clone 메서드를 이용해서 todo의 객체 상태를 전부 복사하여 새로운 todo 객체를 만들어 todoItem 컴포넌트에 전달하고 있습니다. 이러한 방법을 방어적 복사(defensive copy)라고 합니다.

    드디어 코드는 의도한대로 동작하겠지만, 비교문은 여전히 복잡하며 매번 객체를 전체적으로 복사하는건 성능면에서 좋지 않습니다. 또, 객체의 전달 방식이나 사용 방식을 예의주시해야하는 번거로움도 수반됩니다.

    불변 객체일 때

    이제 todos 모델 객체의 update 메서드를 Immutable.js를 이용해 불변 객체로 관리하도록 변경해보겠습니다.

    // todos.js
    class Todos extends events.EventEmitter {
        constructor() {
            this._todos = new Immutable.List();
        }
    
        // ... 생략 ...
    
        update(id, label) {
            // 새로운 List 객체를 생성한다.
            this._todos = this._todos.update(
                this._todos.findIndex(t => t.get('id') === id),
                t => t.set('label', label) // 새로운 todo 객체를 생성한다.
            )
    
            this.emit('update');
        }
    }

    todos 객체의 생성자 메서드를 통해 Immutable.js의 List 객체를 생성하고 있습니다. 특정 todo 객체의 값을 변경할 때는 List 객체의 update 메서드를 이용해 새로운 상태를 갖는 todo 객체와 List 객체를 다시 생성하여 설정합니다.

    // todoItem.js
    shouldComponentUpdate(nextProps, nextState) {
        return (
            nextProps.todo !== this.props ||
            nextState.label !== this.state.label
        );
    }

    이제 비교문이 다시 단순해졌습니다. 객체의 상태가 변하지 않는 한 참조는 항상 같을 것이고, 객체의 상태가 변경될때만 새로운 객체가 생성되므로 참조가 달라집니다. 따라서 단순히 참조만 비교하는 것 만으로도 객체의 상태가 변경됐는지 판단할 수 있습니다.

    매번 객체를 새로 생성하면 메모리 관리 시스템에 부담을 줄 수 있다고 생각할 수 있지만 이 점이 시스템 전체적인 병목을 일으키진 않습니다. 오히려 객체의 값을 전체적으로 복사하는 방어적 복사가 더 부담이 될 수 있습니다.

    정리

    불변 객체는 값을 복사할 필요 없습니다. 객체를 복사할 때는 항상 같은 객체를 참조하는 주소만 반환하면 됩니다. 즉, 객체를 하나 생성하고 이를 지속적으로 재사용할 수 있습니다(Intern) 이처럼 불변 객체는 복사를 단순화할 수 있어 성능적으로 유리할 수 있습니다. 동일한 값을 여러번 복사해도 참조를 위한 포인터 크기 만큼만 메모리가 늘어날 뿐입니다.

    또한 React.js의 shouldComponentUpdate 메서드를 통해 알 수 있듯이 비교문을 크게 단순화할 수 있습니다. 이 점이 React.js에서 불변 객체를 사용했을때 가장 피부로 체감할 수 있는 부분입니다. 단순한 비교문은 코드를 관리하기 쉽게 만들어줍니다. 반면, 가변 객체를 여러 뷰 컴포넌트에서 의존하면 이를 추적하고 관리하기 쉽지 않을 뿐더러 비교문도 작성하기 어렵습니다.

    Flux 아키텍처에서 말하는 단방향 데이터 흐름과 Immutable.js의 불변 객체, 그리고 수동적인 뷰 특징을 가진 리액트 컴포넌트가 한데 어울어지면 보다 단순하고 사고하기 쉬운 프로그램을 작성할 수 있습니다.

    참고

  • 읽기전에...

    이 문서는 koba04님이 작성한 React.js Advent Calendar를 번역한 것입니다. 본래 원문서는 캘린더 형식으로 소개하지만 여기에서는 회를 나눠 작성할 생각입니다. 또한, React 버전 0.12.1 때 작성된 문서이기 때문에 현 버전과 다른 점이 있을 수 있습니다. 최대한 다른 부분을 노트로 작성할 생각이지만, 만약 생략된 부분이 있다면 댓글로 알려주시면 감사하겠습니다.

    React.js + CSS

    React.js 개발자인 vjeux가 「React:CSS in JS」 라는 주제로 발표를 했는데 그 내용이 꽤 흥미있고 React.js와도 관계가 있는 것이기 때문에 소개하고자 합니다. 또다른 React.js 개발자 zpao의 「React Through the Ages」 라는 발표에서도 이 관점에 관해 언급하고 있습니다.

    CSS를 확장할 때의 문제점

    1. Global Namespace
    2. Dependencies
    3. Dead Code Elimination
    4. Minification
    5. Sharing Constantsn
    6. Non-deterministic Resolution
    7. Isolation

    여기에서 말하는 확장은 페이스북 정도의 규모에서 확장을 말하는 것 같습니다.

    Global Namespace

    CSS에서 모든 것은 글로벌 공간에 선언되기 때문에 명명 규칙 등으로 분할할 필요가 있습니다.(부트스트랩은 600개의 전역 이름을 정의하고 있습니다.)

    Dependencies

    컴포넌트와의 의존 관계를 관리하기 힘듭니다. 컴포넌트 내에서 requireCSS 처럼 CSS를 읽어 들이도록 했다고 하더라도 다른 곳에서 이미 그 CSS를 require 했다면 이미 동작하게 됩니다.

    Dead Code Elimination

    미사용 코드를 검출하기 어렵습니다.

    Minification

    class 명의 minification에 관한 것입니다. (저자: 할 필요가 있는지 의문입니다) 이것도 템플릿(HTML or JS)과 CSS를 대응하여 개발할 필요가 있습니다.

    Sharing Constantsn

    CSS와 JS 측에서 변수를 공유하기 어렵습니다.

    Non-deterministic Resolution

    CSS에서는 상세한 속성이 같은 경우 나중에 작성한 것이 우선됩니다. 그래서 requireCSS 등의 구조를 사용해 컴포넌트와 같이 비동기로 CSS를 읽을 경우 읽는 순서에 따라 다르게 출력돼 의도하지 않는 결과가 발생할 수 있습니다.

    <div class="foo bar">xxx</div>
    .foo {color: red}
    .bar {color: blue}
    
    /* or */
    
    .bar {color: blue}
    .foo {color: red}

    이를 회피하기 위해서 상세한 속성을 수정하는 등의 작업이 필요할 수 있습니다.

    Isolation

    React.js에서 Button 컴포넌트를 만들었을 때 이 button 태그의 스타일을 지정하려면 Button 컴포넌트가 어떤 태그 구조로 구현돼 있는지를 알아야 할 필요가 있어 컴포넌트를 잘 분리 할 수 없습니다.

    <div className="foo">
      <Button/> <!-- <div><button>xxx</button></div> -->
    </div>
    .foo > div {
      ...
    }

    그렇다면 CSS in JS

    위와 같은 문제는 Sass 같은 CSS Preprocessor 등을 사용하거나 설계 레벨에서 해결 가능한 것도 있지만, CSS를 JavaScript의 Object 형태로 컴포넌트의 스타일을 지정하는데 사용하면 문제를 해결할 수 있지 않을까 하는 접근법입니다. 즉, 템플릿(HTML)을 JS의 안에 가지고 온 것(JSX)처럼 CSS도 JS 안으로 가지고 오겠다는 뜻입니다.

    var style = {
      container: {
        backgroundColor: '#ddd',
        width: 900
      }
    }
    
    var Container = React.createClass({
      render() {
        return <div style={style.container}>{this.props.children}</div>;
      }
    });

    아래와 같은 함수를 이용하면 조금 더 유연하게 스타일을 지정할 수 있습니다.

    function m() {
      var res = {};
      for (var i=0; i < arguments.length; ++i) {
        if (arguments[i]) assign(res, arguments[i]);
      }
      return res;
    }
    
    <div style={m(
      style.container,
      { marginTop: 10 },
      this.props.isWarning && {color: 'red'}
    )}>xxx</div>

    또, Prop을 공개해 밖에서 스타일을 지정하도록 할 수 있습니다.

    propTypes: {
      style: React.PropTypes.object
    },
    render() {
      return <div style={m(style.container, this.props.style)}>xxx</div>
    }

    스타일의 우선 순위는 순서를 조절하는 것으로 간단히 변경할 수 있습니다.

    propTypes: {
      style: React.PropTypes.object
    },
    render() {
      return <div style={m(this.props.style, style.container)}>xxx</div>
    }

    이처럼 컴포넌트에 직접 지정하는 것으로 상세한 속성 등은 알 필요 없어지고 JavaScript에 가져오는 것으로 프로그래밍적으로 처리 가능하며 공통화나 상속 등도 간단히 실현할 수 있어 그 결과 처음에 언급한 여러가지 문제를 해결할 수 있습니다. 예에서는 스타일을 컴포넌트의 안에 작성했지만 다른 파일에 작성하고 require 해서 사용할 수도 있습니다.

    JavaScript 쪽으로 마크업을 가지고 온 JSX 처럼 CSS도 JavaScript로 가져오자는 이 접근에 관해 어떻게 생각하시나요? 여기까지 CSS in JS를 소개했습니다.

    React.js in future

    React.js의 향후라는 주제로 이번 절을 작성할까 합니다. React.js의 로드맵은 facebook/react와는 다른 저장소 인 react-future에서 논의되고 있습니다. 여기에 있는 것은 어디까지나 아이디어 수준이지만 구체적인 코드로 설명돼 있어서 어떤 모습일지 예측하기 쉽습니다.

    또, 이전 절에서도 소개했던 「React Through the Ages」 슬라이드에서도 React.js의 현재와 미래에 대해서 이야기하고 있으므로 참고하세요.

    지금까지의 React.js

    React.js는 원래 페이스북이 PHP + XML로 만든 XHP 프로젝트에서 시작됐습니다. 이를 JavaScript에 가져온 것이 React.js입니다. 애플리케이션 전체적으로 rerender 하는 구조는 서버 측의 rendering 방식과 비슷하다는 점에서도 이런 흐름을 예측할 수 있습니다. 또, React.js는 최초엔 Standard ML로 만들어졌다가 그 뒤 Haxe가 되어 지금의 Pure한 JavaScript가 됐습니다.

    1.0과 그 앞

    「React Through the Ages」를 보면 API의 안정화와 삭제 그리고 ES6, 7 사양을 따르려고 하는 의도를 느낄 수 있습니다. ES6, 7에서 사용할 수 있는 기능을 최대한 활용하여 React.js 자체에서는 부가적인 처리를 하지 않겠다는 방향성을 엿볼 수 있습니다.

    class Button extends React.Component {
      getInitialState() {
        return {count: 0};
      },
      render() {
        return (
          <button onClick={() => this.setState({count: this.state.count + 1}) }>
            {this.state.count}
          </button>
        );
      }
    }
    • CSS in JS : 이는 이전 절에서 소개한 CSS의 문제를 해결하기 위한 접근 방식입니다.
    • Web Workers : VirtualDOM 계산을 WebWorkers에서 하는 것으로 UI 단에 좋은 영향을 줄 수 있다면 도입하고 싶다고 합니다.
    • Layout & Animation : 어떠한 방식으로 정의하도록 하느냐가 어려운 문제이지만 중요한 기능이기 때문에 지원하고 싶다고 합니다.
    • M(V)C : 자신(페이스북)은 필요하지 않지만 많은 개발자가 React.js를 사용했을 때의 MVC의 M과 C에 대해 논의하거나 개발하고 있는 것을 보고 이에 대한 지원도 중요한 사항으로 여기는 것 같습니다. React.js가 풀-프레임워크가 되는 일은 없을 것 같습니다만…
    • Other : 이 외에도 새로운 테스트 지원이나 문서, Immutable Data 등 다양한 아이디어가 있는 것 같습니다.

    React.js의 미래

    react-future의 저장소를 보면 ES6, 7의 기능을 도입할 경우의 형태를 볼 수 있습니다. 단, 여기에서 소개하는 기능은 아직 구현돼 있지 않고 합의된 것도 아니기 때문에 이렇게 지원된다고 장담할 순 없습니다.

    역자노트

    일부 기능은 이미 사용할 수 있습니다. 원문이 2014년 12월에 작성됐다는 사실을 감안해주세요.

    Class

    import {Component} from 'react';
    
    export class Button extends Component {
      props : {
        width: number
      }
      static defaultProps = {
        width: 100
      }
      state = {
        counter: Math.round(this.props.width / 10)
      }
      handleClick = (event) => {
        event.preventDefault();
        this.setState({counter: this.state.counter + 1});
      }
      render(props, state) {
        return (
          <div>
            This button has been clicked: {state.counter} times
            <button onClick={this.handleClick} style={{ idth: props.width}}/>
          </div>
        );
      }
    }

    ES6의 Module이나 Class, ArrowFunction 등이 사용됐고 React.js 독자적인 부분이 적어졌습니다. 또 props의 형 지정 방식도 변경 됐는데 이는 facebook/flow와 연계될 수도 있을 것 같습니다. (댓글에는 TypeScript compatible syntax로 작성돼 있지만) 또, render에 props와 state를 인자로 전달하는 것 같은 형태로 돼 있습니다.

    mixin

    import { mixin } from 'react-utils';
    
    const A = {
      componentDidMount() {
        super();
        console.log('A');
      }
    };
    
    class B extends mixin(A) {
      componentDidMount() {
        console.log('B');
        super();
      }
    }
    
    new B().componentDidMount(); // B, A
    
    import { Component } from 'react';
    
    class Component extends mixin(Component, C) {
      render() {
        return <div/>;
      }
    }

    mixin은 util로써 준비하고, super로 부모의 것을 호출하는 식으로 디자인돼 있습니다. state의 merge 방식에 관한 문제가 있는 것 같습니다.

    Stateless Functions

    export function Button(props : {width: number, onClick: function}) {
      return (
        <div>
          Fancy button
          <button onClick={props.onClick} style={{width: props.width}}/>
        </div>
      );
    }

    Prop 만을 갖는 Stateless한 컴포넌트는 Prop을 전달받는 함수로써 정의할 수 있도록 돼 있습니다.

    Elements

    JavaScript 객체 문법이나 JSX 이외에도 여러가지 방법으로 React Element를 작성할 수 있도록 하고자 하는 바램이 있는 것 같습니다.

    Object 리터럴
    {
      type: Button,
      props: {
        foo: bar,
        children: [
          { type: 'span', props: { children: a } },
          { type: 'span', props: { children: b } }
        ]
      },
      // optional
      key: 'mybutton',
      ref: myButtonRef
    }
    Native Components

    React.DOM 이하의 API는 없어지고 단순한 문자열로 정의할 수 있도록 돼 있습니다. 또 Web Components의 커스텀 태그에도 호환성을 지니게 돼 있습니다.

    Template Strings

    ES6의 Template Strings을 이용해 정의할 수 있도록 돼 있습니다.

    X`
     <my-button foo=${bar} key="mybutton" ref=${myButtonRef}>
       <span>${a}</span>
       <span>${b}</span>
     </my-button>
    `

    이외에도 여러가지 소개하고 있으므로 흥미가 있다면 꼭 한번 읽어보시길 바랍니다.

    React.js에 관한 리소스, 그리고 정리

    여기까지 React.js를 소개했습니만, 조금이라도 사용하는데 참고가 됐다면 좋겠습니다. React.js는 facebook, instagram이나 Github의 AtomEditor 물론, 「Atlassian」, 「Netflix」, 「Reddit」, 「The New York Times」, 「Yahoo」 등 많은 곳에서 사용하고 있는 것 같습니다(참고).

    또, 내년 1월말에는 React.js Conf가 있으므로 여러가지 소식이 공유되고 점점 분위기도 무르익을 것으로 생각됩니다. 내년도 즐거운 한해가 될 것 같습니다.

    React.js의 공식 블로그#reactjs 해쉬 태그를 구독하면 여러가지 정보를 모을 수 있습니다.

    리소스 정리

    개인적으로 읽고 재미있었던 것이나 공식적인 사이트를 정리해봤습니다.

    공식 사이트

    컴포넌트, 샘플 모음집

    • React Coponents: 공개된 React.js 컴포넌트가 정리돼 있습니다.
    • React Rocks: React.js의 샘플이나 데모가 모여있습니다.

    입문

    Virtual DOM

    Flux

    • Isomorphic Flux: Yahoo가 Isomorphic한 Flux 애플리케이션을 만든 이야기입니다.
    • flux-meetup: 페이스북의 개발자가 하는 React.js와 Flux에 관한 설명입니다.

    Developer tool

    소개하는 것을 깜빡 잊고 있었습니다. React.js 개발을 할 때에 편리하게 사용할 수 있는 크롬 확장 도구인 React Developer Tools도 있습니다.

    아래 그림과 같이 React.js를 사용하는 페이지에 가면 개발자 도구에 React탭이 표시되고 거기서 HTML의 태그가 아니라 Component로 볼 수 있습니다. 또, Prop과 State및 EventListener와 Component의 값도 확인 할 수 있어 편리하게 디버깅할 수 있습니다.

    React Developer Tools
    <그림 1 React Developer Tools>

    정리

    역시 읽을 때와 번역해서 공유할 때 느낌은 많이 다르네요. 알아서 이해했던 것들도 신경 써야 하니 시간이 좀 걸렸습니다. koba04님의 React.js Advent Calendar는 제가 처음 React.js를 학습할때 도움을 받았던 문서였기 때문에 무엇부터 차근차근 봐야 할지 모르시는 분들이 있을 것 같아서 일본어 문서를 번역했습니다. React.js를 이해하는데 많은 도움이 되길 간절히 바라면서 이만 마치도록 하겠습니다.

    여기까지 React.js를 소개했습니다. 끝까지 읽어주셔서 감사합니다!