리액트 따라하려다 바짓가랑이 찢어진 이야기 - 2주차 회고

and.Y·2020년 8월 25일
2

우아한 테크캠프

목록 보기
3/4
post-thumbnail

리액트 따라하려다 바짓가랑이 찢어진 이야기

본 글은 '리액트'를 따라만든 wooact를 만들려다 실패한 이야기와 더불어 함께 고생한 동욱님께 미안함을 전달하는 내용을 담고 있습니다. wooact를 이용해서 프로젝트를 마무리 했으니 사용할 수 없다고는 할 수 없으나, 퍼포먼스 상의 이슈를 담고 있기에 조심해서 사용하시길 권장드립니다. 현재 수준은 리액트와 비슷한 사용환경을 구성해서 익숙하고 깨끗한 코드를 작성하시는 데에는 확실한 도움을 드릴 수 있을 정도입니다. 스스로도 개선을 위한 노력을 할 예정이지만, 개선 사항에 대한 조언 및 PR은 적극 환영입니다. 그럼 지금부터 지난 2주 간의 프로젝트 회고 및 바짓가랑이가 어떻게 찢어졌는지에 대한 이야기를 들려드리겠습니다.

시작

언제나 그렇듯 목표는 어차피 반도 못할테니 높고 위대해야 합니다. 그라운드 룰을 정하는 과정에서 제일 먼저Typescript의 사용을 제안했습니다. 기본적인 파라미터와 리턴 값 설정만 해도 엄청난 자동완성 기능으로 오타를 줄이고 생산성의 향상을 분명 느낄 수 있을 것이라고 했습니다. 이 부분에 대해서는 동욱님도 프로젝트가 끝난 후에 그 이점을 느끼셨다니 뿌듯함이 이로 말할 수 없습니다. 다음 카드는 TDD(Test Driven Development)였습니다. 스스로도 테스팅을 해야지라고 수백 번 얘기하고, 수천 번 하지 않았기에 프로젝트에 해당 룰을 강제하면 어떻게든 하게 되지 않을까. 첫째 주에 함께 작성한 코드에는 모두 테스트 코드를 먼저 작성하고, 빨간 불을 초록 불로 바꾸는 미학을 경험했습니다. 두번째 주는 잘 될 것이라는 믿음에서 시간의 압박을 경험하고는 바로 테스트를 생략하게 되었습니다. 어차피 다들 목표한 것을 100프로 이룬 팀은 없을테니, 이 정도는 애교로 넘어갈 수 있지 않을까 생각합니다. 문제는 대망의 wooact였습니다.

잘못된 전제

wooact를 만들자고 제안한 것에는 이러한 전제가 깔려 있었습니다. 동욱님은 리팩토링을 적어도 한 두번 해서 꼭 깔끔한 코드를 짜고 싶다고 하셨습니다. 리액트는 동욱님도 저도 이용해본 라이브러리이고, 하나의 컴포넌트 안에서 데이터와 뷰의 구성을 함께해서 누가 봐도 이해하기 쉬운 코드를 작성할 수 있지 않을까 생각으로 리액트 흉내내기를 해보자고 제안했습니다. 마지막으로는 환경을 구성하는 것에는 시간이 걸리겠지만, 익숙한 환경만 구성하면 생산성이 급격히 올라가지 않을까? 라는 엉망진창의 전제도 깔려있었습니다. 이러한 이유로 저희는 가볍게 리액트를 만들어보자는 도전을 하게 되었습니다. 하지만 지금 쯤 눈치 빠르신 분들은 궁금 하셨을 제일 중요한 전제 하나를 빼먹고 시작했습니다. 리액트는 페이스북이 만들었고, 해당 라이브러리의 코드는 약 30만 줄 이었습니다. 당연히 순수 js파일들의 코드 양만 계산했습니다. 만든 사람과 만들려고 하는 사람의 실력 차이, 제한된 시간, 프로젝트의 목표가 이것이 아닌 하나의 도구에 불과한 상황, 수 많은 프로젝트의 요구조건 등 이렇게 중요한 많은 전제들을 생각지 못했고, 그 결과는 두번째 주 저희의 잠을 앗아갔습니다.

Wooact

완성된 wooact는 이렇게 사용할 수 있습니다.

저희의 목표는 JSX가 너무 멋지니까, createElement함수를 wrapping해서 jsx와 비슷한 문법을 이용할 수 있게 만들고, state값이 바뀔 때 마다 다시 렌더링 되게 하는 핵심 기능만(?)을 구현하기로 낮게(?) 잡았습니다. 위의 사진에서 예시로 보실 수 있듯, createElementappendChild등의 api로 지저분한 코드를 작성하지 않을 수 있었고, JSX와 비슷한 문법을 이용할 수 있었습니다. 또한 state의 변경에 따라 다시 렌더링 되도록 해당 컴포넌트가 extends하는 Componen 추상 클래스에서 이러한 작업을 해주고 있습니다.

첫 주에 함께 wooact 를 만드는 과정은 상당히 재밌고 훌륭했다고 평가하고 싶습니다. 저희는 앞서 말한 그라운드 룰에 따라 typescript로 코드를 작성하였고, TDD와 페어 프로그래밍을 결합하여 작업을 시작했습니다. 무엇을 만들까, 어떻게 만들까에 대한 논의의 결과를 테스트 코드로 작성하고, 돌아가며 빨간 불(실패한 테스트)를 초록 불(성공한 테스트)로 바꾸면서 개발했습니다. 서로 충분한 논의와 서로가 알고 있는 지식을 공유하며 함께 개발하는 즐거움을 체험했습니다. 실제 wooact는 첫째 주 화요일부터 개발에 시작하여, 자료조사 및 개발에 약 3일 정도를 할애했습니다. 즉 금요일 데모에서 보여줄 수 있는 작품은 없었습니다. 서버는 폴더만 존재했고, 화면에 무언가를 띄워 놓을 것 조차 없는 상태였습니다. 그래도 행복회로를 돌리며 우리의 전제에 따라 환경을 구성했으니, 한 주면 모든 것을 만들 수 있지 않을까 하는 기대감은 여전했습니다. 하지만 세상만사 좋은 일만 있을 수는 없으니 금요일 데모를 준비하며 바로 거대한 문제점을 발견합니다.

우리는 페이스북이 아니다

페이스북도 3일 만에 출퇴근 시간을 정확히 지키면서 리액트를 만들지 않았을 것은 분명합니다. 그것을 저희가 해냈다고 생각했으니 당연히 말이 안되죠. 현재 wooact는 스테이트가 변경되면 스테이트를 이용하고 있는 엘리먼트가 변경되지 않고, 컴포넌트 자체가 다시 렌더됩니다.

잠시 리액트와의 격차를 실감하기 위해 저희가 구현한 부분에 대하여 리액트가 어떻게 작동하는지 간략한 설명을 드리자면, React.createElement는 virtual Dom 이라고 불리는 JSX를 object형식으로 변환한 값을 리턴합니다. 해당 오브젝트를 실제 돔에 렌더하는 것은 ReactDom.render 메소드가 담당하고 있습니다. 해당 렌더함수는 트리형태로 저장된 오브젝트를 순회하면서 내부적으로 값이 다른 것을 찾아내는 diffing 알고리즘(핵심)을 적용하여 해당 엘리먼트만 바꾸거나 추가 혹은 삭제해서 다시 렌더하게 됩니다. 심지어 트리를 순회해서 다른 것을 찾아내는 diffing알고리즘을 다른 곳에서는 O(n**3) 으로 할 때 자기들은 O(n)에 한다고 첫줄에 써놓았으니, 퍼포먼스 측면에서 비교도 안될 수준입니다. 페이스북은 대단합니다. 박수

interface IState {
  menuVisible: boolean
	...
}

class App extends Component<IProps, IState> {
	...
  onToggleSideMenu = () => {
    this.setState('menuVisible', !this.getState('menuVisible'))
  }
	...
  render() {
    return div(
      {
        className: 'todo-container',
      },
      new Header({
        title: '우와한 투두',
        onToggleSideMenu: () => this.onToggleSideMenu(),
      }),
	    ...
    )
  }
}

그럼 도대체 뭐가 문제였냐하니, 위의 코드는 데모를 준비하는 과정에서 Header에 존재하는 Menu버튼을 눌렀을 때, log들을 볼 수 있는 창을 띄우는 것을 테스트하는 코드입니다. 리액트를 한번이라도 이용해보신 분들은 큰 무리없이 이해하실 수 있으리라 생각합니다. 그런데 어떻게 해도 오른쪽에서 왼쪽으로 밀려 오거나 들어가는 애니메이션이 적용되지 않는 것을 확인했습니다. 심지어 브라우저에서 css값 들을 수정하면서 테스트 해보면 정상적으로 작동하는 것을 확인했으니, 도대체 무엇이 문제인지 알 수가 없었습니다. 하지만 위의 설명을 이해하셨다면 그 이유를 알게 되셨을 겁니다. 네 저희는 menuVisible이라는 스테이트가 변경될 때, div로 시작하는 하위의 모든 엘리먼트가 다시 렌더됩니다 ㅎㅎ. 당연히 클래스가 새로 추가된 엘리먼트는 transition이 적용될 수 없는 것이죠. 이것을 데모 30분 전에 발견하고, 프로젝트를 없어야 하나 고민하기 시작했습니다.

적정타협

수요일에 만난 우형 개발자 분께서 말씀하셨듯, 프로젝트에 적정 기술을 찾는 것이 중요하다고 말씀하셨습니다. 이에 따라 우테캠 개발자로 저희는 적정 타협을 하게 되었습니다. 스테이트의 변경을 최소화하고, 컴포넌트의 단위를 최대한 작게 만들어서 렌더링 이슈를 해결하자. 그러다 보니 아래와 같이 괴랄한 코드를 작성하게 되었지만, 이용할 수 있는 수준이 되었습니다.

class AddItemInput extends Component<IProps, IState> {

...
async onSubmit(e: Event) {
    const { itemId, kanbanId } = this.props
    const inputElement = this.element.querySelector(
      '.box-input'
    ) as HTMLTextAreaElement
    const content = inputElement.value

    if (itemId) {
      await updateItem({ id: itemId, content })
    } else {
      await createItem({ kanbanId: kanbanId, content })
    }

    await window.dispatchEvent(new Event('item_changed'))
  }

...
render() {
	const addButton = new BoxButton({
	  type: 'positive',
	  buttonText: this.props.itemId ? 'Update' : 'Add',
	  onClickHandler: (e: Event) => this.onSubmit(e),
	  clickAble: this.props.itemId !== null,
	})

	return div({}, 
		...
		addButton
		...
	)
}

적정한 타협을 거친 후 익숙한 환경이 조성되었기에 클라이언트의 개발 속도는 빨랐습니다. 우여곡절 끝에 데모 10분전까지 개발을 하고서야 당연히 버그 덩어리에 부족한 게 많지만 완성할 수 있었습니다. 아 당연히 출퇴근 시간은 존재하지 않았습니다. 지하철에서도 코딩을 하기 위해 비교적 혼잡이 덜한 반대방향으로 지하철을 타기 시작했고, 새벽 1~2시에 PR을 보내고, review를 해주는 아름다운 워워밸을 실현했습니다. 눈을 감으면 잠, 눈을 뜨면 코딩. 아 그래도 밥먹을 때 개발 얘기를 하지 않겠다는 그라운드 룰은 끝까지 지켰군요 동욱님. 다시 한번 고생하셨습니다.

본격 후기

길었던 글의 끝을 내보려고 합니다. 욕심이 많았던 탓에 더 어렵게 돌아오지 않았나 하는 생각이 제일 먼저 듭니다. 그래도 크롱님께서 약간의 격려를 해주신 덕분에 실패한 경험이지만 당당하게 공유해 보았습니다. 제일 먼저 너무 당연하게 쓰던 것들을 만들어 보니까, 어떻게 작동되는지 공부해 볼 시간을 가질 수 있었던 게 가장 좋았습니다. 제한된 상황 때문에 하게 된 도전이지만, 도전의 결과는 충분히 보람찼습니다. 이왕 시작한 거 조금 더 괜찮게 만들어 보기 위해, 가볍게 리액트의 핵심인 diffing 알고리즘을 구현해 보려고 합니다. 또한 리액트도 모든 이벤트를 윈도우에 붙이고 위임하는 방식을 이용한다고 하는데, 이것도 구현해 보려고 합니다. 말로만 하고 안하지 않게 혹시 길을 가다 마주치면 잘되가요? 라고 한번씩 채찍질 부탁 드립니당. 아 물론 개선 사항에 대한 조언 및 PR은 적극 환영입니다(시작내용 복붙). 중간중간 떠든게 많아 뭐라고 끝을 내야할지 모르겠습니다. 사랑하고 미안합니다. 동욱님. 끝

0개의 댓글