tag: 개발 이야기

리액트 훅(Hooks): 제 테스트는 어떻게 되나요?

새로 나온 훅 기능에 대비해서 테스트는 어떻게 준비할지 알아봅니다 번역

2019년 3월 21일

이 포스트는 Kent C. DoddsReact Hooks: What's going to happen to my tests?를 번역한 글입니다.

새로 나오는 리액트의 훅(hook) 기능에 대해 가장 많이 받는 질문 중 하나가 바로 테스팅입니다. 만약 지금 테스트가 다음과 같은 모양이라면 그 걱정을 충분히 이해할 수 있습니다.

// 이전 블로그 포스트에서 빌린 예제
// https://kcd.im/implementation-details
test('setOpenIndex sets the open index state properly', () => {
  const wrapper = mount(<Accordion items={[]} />)
  expect(wrapper.state('openIndex')).toBe(0)
  wrapper.instance().setOpenIndex(1)
  expect(wrapper.state('openIndex')).toBe(1)
})

이 enzyme 테스트는 Accordion이 클래스 컴포넌트며 instance가 존재하는 경우에만 동작합니다. 하지만 함수 컴포넌트라면 "인스턴스"라는 개념이 존재하지 않습니다. 그러므로 상태와 생명주기가 있는 클래스 컴포넌트에서 훅을 사용한 컴포넌트로 리팩토링했을 때에는 이 테스트의 .instance().state()가 동작하지 않을 겁니다.

이제 Accordion 컴포넌트를 함수 컴포넌트로 리팩토링 한다면 이 테스트는 고장 나게 됩니다. 그럼 테스트를 던지거나 다시 작성하지 않고 훅을 사용해서 코드를 리팩토링할 방법은 없을까요? 위 테스트처럼 컴포넌트의 인스턴스를 참조하는 enzyme API의 사용을 피하는 일에서부터 시작할 수 있습니다. 이 내용은 제 "구현 상세(implementation details)" 포스트에서 확인할 수 있습니다.

이제 클래스 컴포넌트의 간단한 예제를 확인해봅시다. 제가 가장 좋아하는 <Counter /> 컴포넌트 예제는 다음과 같습니다.

// counter.js
import React from 'react'

class Counter extends React.Component {
  state = {count: 0}
  increment = () => this.setState(({count}) => ({count: count + 1}))
  render() {
    return <button onClick={this.increment}>{this.state.count}</button>
  }
}

export default Counter

이제 이 컴포넌트를 테스트하는데 어떤 방식으로 작성하면 훅이든 클래스 컴포넌트든 상관없이 테스트할 수 있을까요?

// __tests__/counter.js
import React from 'react'
import 'react-testing-library/cleanup-after-each'
import {render, fireEvent} from 'react-testing-library'
import Counter from '../counter.js'

test('counter increments the count', () => {
  const {container} = render(<Counter />)
  const button = container.firstChild
  expect(button.textContent).toBe('0')
  fireEvent.click(button)
  expect(button.textContent).toBe('1')
})

이 테스트는 통과할 겁니다. 이제 동일한 컴포넌트를 훅으로 작성해보겠습니다.

// counter.js
import React from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  const incrementCount = () => setCount(c => c + 1)
  return <button onClick={incrementCount}>{count}</button>
}

export default Counter

이 테스트는 구현 상세를 피해서 테스트하고 있기 때문에 훅으로 만든 테스트도 문제 없습니다. 정말 멋지지 않나요? :)

useEffectcomponentDidMount + componentDidUpdate + componentWillUnmount가 아닙니다.

useEffect 훅을 사용하기 전에 고려해야 하는 점이 있습니다. 이 훅은 조금 특별하고 다르고 멋지기 때문인데요. 클래스 컴포넌트에서 훅으로 리팩토링할 때에 componentDidMount, componentDidUpdate, componentWillUnmount를 하나 이상의 useEffect 콜백 함수로 바꾸게 될 겁니다. (컴포넌트의 수명주기에 포함된 컴포넌트의 관심사에 따라 다릅니다.) 하지만 이 코드는 엄밀히 말해서 리팩토링이 아닙니다. "리팩토링"이 무엇인지 빠르게 살펴보도록 하겠습니다.

코드를 리팩토링할 때는 사용자가 볼 수 있는 변화를 만들지 않고 내부 구현을 바꿔야 합니다. 다음은 위키피디아가 말하는 "코드 리팩토링"입니다.

코드 리팩토링은 존재하는 컴퓨터 코드를 구조를 조정하는 과정으로 외부 동작을 바꾸지 않고 팩토링 즉, 분해(decomposition)를 수행하는 일을 의미합니다.

이제 다음 코드에서 개념을 살펴봅니다.

const sum = (a, b) => a + b

이제 이 함수를 리팩토링해 보도록 하겠습니다.

const sum = (a, b) => b + a

앞서 함수와 동일하게 동작하지만 구현은 약간 다릅니다. 근본적으로 이 과정이 "리팩토링"입니다. 다음은 리팩토링이 아닌 경우입니다.

const sum = (...args) => args.reduce((s, n) => s + n, 0)

멋진 코드입니다. sum은 더 멋지게 동작하지만, 엄밀히 따지면 리팩토링이 아니라 개선입니다. 비교해봅시다.

호출이전 결과이후 결과
sum()NaN0
sum(1)NaN1
sum(1, 2)33
sum(1, 2, 3)36

왜 이 변경은 리팩토링이 아닌가요? 그 이유는 "외부 동작을 변경"했기 때문입니다. 새 코드는 이전 코드보다 바람직하지만 기존 동작을 바꿨습니다.

그러면 왜 이 내용이 useEffect를 사용하는 것과 어떤 관련이 있을까요? 앞서 본 카운터 클래스 컴포넌트에서 새로운 기능을 추가한 예제를 살펴보도록 하겠습니다.

class Counter extends React.Component {
  state = {
    count: Number(window.localStorage.getItem('count') || 0),
  }
  increment = () => this.setState(({count}) => ({count: count + 1}))
  componentDidMount() {
    window.localStorage.setItem('count', this.state.count)
  }
  componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) {
      window.localStorage.setItem('count', this.state.count)
    }
  }
  render() {
    return <button onClick={this.increment}>{this.state.count}</button>
  }
}

이 코드에서는 count의 값을 componentDidMountcomponentDidUpdate를 사용해서 localStorage에 저장합니다. 구현 상세에 영향받지 않는 테스트는 다음과 같습니다.

// __tests__/counter.js
import React from 'react'
import 'react-testing-library/cleanup-after-each'
import {render, fireEvent, cleanup} from 'react-testing-library'
import Counter from '../counter.js'

afterEach(() => {
  window.localStorage.removeItem('count')
})

test('counter increments the count', () => {
  const {container} = render(<Counter />)
  const button = container.firstChild
  expect(button.textContent).toBe('0')
  fireEvent.click(button)
  expect(button.textContent).toBe('1')
})

test('reads and updates localStorage', () => {
  window.localStorage.setItem('count', 3)
  const {container, rerender} = render(<Counter />)
  const button = container.firstChild
  expect(button.textContent).toBe('3')
  fireEvent.click(button)
  expect(button.textContent).toBe('4')
  expect(window.localStorage.getItem('count')).toBe('4')
})

테스트가 통과했습니다! 이제 이 컴포넌트를 "리팩토링"하는데 훅을 사용해보겠습니다.

import React, {useState, useEffect} from 'react'

function Counter() {
  const [count, setCount] = useState(() =>
    Number(window.localStorage.getItem('count') || 0),
  )
  const incrementCount = () => setCount(c => c + 1)
  useEffect(() => {
    window.localStorage.setItem('count', count)
  }, [count])
  return <button onClick={incrementCount}>{count}</button>
}

export default Counter

좋습니다. 사용자의 관점에서 보면 이 컴포넌트는 이전과 똑같이 동작할 겁니다. 하지만 이 변경은 이전의 동작과는 다릅니다. 여기서 useEffect 콜백은 이후에 구동하도록 예정 되었습니다. 이전 코드에서는 컴포넌트가 렌더링 된 후에 값을 localStorage에 동기적으로 저장했습니다. 새 코드에서는 렌더링 된 후에 호출되도록 변경했습니다. 왜 그럴까요? 리액트 훅 문서에 설명된 부분을 확인해봅니다.

componentDidMountcomponentDidUpdate와 다르게 useEffect로 예정한 효과는 브라우저가 화면을 업데이트하는 것을 막지 않습니다. 앱이 더 높은 응답성을 제공할 수 있도록 이렇게 동작합니다. 대부분의 효과는 동기적으로 구동될 필요가 없습니다. 동기적으로 동작하는 경우는 레이아웃을 측정해야 한다거나 하는 등 특수한 경우에 해당합니다. 이런 코드는 useLayoutEffect에 넣을 수 있으며 useEffect와 동일하게 동작하는 API입니다.

이제 useEffect를 사용해서 더 나은 성능을 얻을 수 있게 되었습니다! 멋지지 않나요! 향상된 컴포넌트를 작성한 것 뿐만 아니라 더 가볍게 구동되는 컴포넌트 코드를 작성했습니다.

하지만 이 과정은 리팩토링이 아닙니다. 실제로는 컴포넌트의 동작을 변경했기 때문입니다. 최종 사용자의 입장에서는 이 변화를 눈치채기 힘듭니다. 테스트를 작성할 때 구현 상세를 피하려고 노력했다면 이런 변화 또한 테스트에서 관측되지 않을겁니다.

다른 두 컴포넌트를 문제 없이 테스트가 가능한 이유는 react-dom/test-utils에서 제공하는 새 act 유틸리티 덕분입니다. react-testing-library는 이 유틸리티와 통합되어 있습니다. 그 덕분에 위 테스트가 문제 없이 통과할 수 있는 것입니다. 그래서 구현 상세에 의존하지 않는 테스트를 계속 작성할 수 있으며 소프트웨어를 기존에 개발하던 방식과 큰 차이 없이 개발을 계속 할 수 있게 되었습니다.

렌더링 프로퍼티 컴포넌트는 어떻게 테스트하죠?

이 부분이 제가 좋아하는 부분입니다. 프로퍼티를 렌더링하는 간단한 카운터 컴포넌트를 확인합니다.

class Counter extends React.Component {
  state = {count: 0}
  increment = () => this.setState(({count}) => ({count: count + 1}))
  render() {
    return this.props.children({
      count: this.state.count,
      increment: this.increment,
    })
  }
}
// 사용방법:
// <Counter>
//   {({ count, increment }) => <button onClick={increment}>{count}</button>}
// </Counter>
// __tests__/counter.js
import React from 'react'
import 'react-testing-library/cleanup-after-each'
import {render, fireEvent} from 'react-testing-library'
import Counter from '../counter.js'

function renderCounter(props) {
  let utils
  const children = jest.fn(stateAndHelpers => {
    utils = stateAndHelpers
    return null
  })
  return {
    ...render(<Counter {...props}>{children}</Counter>),
    children,
    // 이 부분으로 컴포넌트 내부의 증감과 횟수에 접근할 수 있는 방법을 제공합니다
    ...utils,
  }
}

test('counter increments the count', () => {
  const {children, increment} = renderCounter()
  expect(children).toHaveBeenCalledWith(expect.objectContaining({count: 0}))
  increment()
  expect(children).toHaveBeenCalledWith(expect.objectContaining({count: 1}))
})

이제 이 컴포넌트를 훅을 사용해서 리팩토링 합니다.

function Counter(props) {
  const [count, setCount] = useState(0)
  const increment = () => setCount(currentCount => currentCount + 1)
  return props.children({
    count: count,
    increment,
  })
}

좋습니다. 앞서 작성한 테스트를 문제 없이 통과합니다. 하지만 리액트 훅: 렌더링 프로퍼티는 어떻게 되나요? (역자 주: 번역)에서 배운 것처럼 커스텀 훅(custom hook)은 리액트에서 코드 공유에 더 나은 자료 형식(primitive)입니다. 위 코드를 커스텀 훅으로 다시 작성해보도록 하겠습니다.

function useCounter() {
  const [count, setCount] = useState(0)
  const increment = () => setCount(currentCount => currentCount + 1)
  return {count, increment}
}

export default useCounter

// 사용방법:
// function Counter() {
//   const {count, increment} = useCounter()
//   return <button onClick={increment}>{count}</button>
// }

멋집니다... 하지만 이제 useCounter는 어떻게 테스트 하나요? 새 useCounter 훅을 사용한다고 코드 전체를 갱신할 수는 없습니다. <Counter />라는 렌더링 프로퍼티 기반 컴포넌트가 수 백 군데에 위치하면 어떻게 하죠? 정말 최악일 겁니다.

제가 도와드리죠. 다음처럼 해결하세요.

function useCounter() {
  const [count, setCount] = useState(0)
  const increment = () => setCount(currentCount => currentCount + 1)
  return {count, increment}
}

const Counter = ({children, ...props}) => children(useCounter(props))

export default Counter
export {useCounter}

이제 새로 작성한 <Counter /> 렌더링 프로퍼티 기반 컴포넌트는 기존에 사용하던 컴포넌트와 완전히 동일한 형태입니다. 이 방법이 진정한 의미의 리팩토링이죠. 또한 이제 누구든지 useCounter 커스텀 훅을 사용해서 코드를 개선하는 것도 가능하게 되었습니다.

더 대단한 점은 앞서 작성한 테스트가 여전히 통과한다는 점입니다. 멋지지 않습니까?

이제 모든 코드를 새로운 훅을 사용해서 갱신하고 나면 더이상 Counter 함수는 필요가 없어지겠죠? 물론 이 함수를 제거할 수도 있지만 그대로 __tests__ 폴더로 옮겨도 좋을 겁니다. 이 방법이 바로 커스텀 훅을 테스트하는 방법이기 때문입니다. 저는 커스텀 훅을 사용해서 렌더링 프로퍼티 기반 컴포넌트를 만드는 것을 선호하는데 실제로 렌더링 된 결과를 함수 호출 결과와 비교하는 데 유용하기 때문입니다.

흥미로운 기법이죠? 저의 새로운 egghead.io 코스에서도 확인할 수 있습니다.

그럼 훅 라이브러리는 어떻게 하죠?

일반적인 훅이나 오픈소스 훅을 작성한다면 특정 컴포넌트를 생각하지 않고서 테스트를 작성해야 합니다. 그런 경우에는 react-hooks-testing-library를 사용하길 권합니다.

결론

코드를 리팩토링하기 전에 할 수 있는 가장 좋은 일은 좋은 테스트 슈트(suite)와 타입 정의를 준비해서 우연히 무언가를 고장 내더라도 그 실수를 바로 알아차릴 수 있도록 하는 일입니다. 하지만 리팩토링을 할 때 테스트 슈트를 집어 던진다면 아무런 의미 없는 일이 되고 맙니다. 제가 드릴 수 있는 조언은 테스트 내에서 구현 상세를 피하라입니다. 지금 클래스 컴포넌트와도 동작하고 미래에 훅으로 만든 함수가 오더라도 동작하는 테스트를 작성하세요.

애플리케이션 상태 관리

소프트웨어에서 가장 복잡한 문제 중 하나인 상태 관리를 React에서는 어떻게 접근하는지 알아봅니다 번역

2019년 3월 20일

이 포스트는 Kent C. DoddsApplication State Management을 번역한 글입니다.

소프트웨어 개발에서 가장 어려운 부분 중 하나는 상태 관리(managing state)입니다. 만약 사용자가 애플리케이션과 상호작용을 전혀 하지 않았더라면 우리 삶은 훨씬 단순했을 겁니다. 물론 90년대 웹사이트에서나 할 이야기입니다. 우리는 상호작용이 필요한 애플리케이션을 만들어야 하니 어디엔가 상태를 저장해야 합니다.

상태는 정말 복잡하며 애플리케이션마다 사용 사례가 극과 극으로 다릅니다. 그래서 애플리케이션에서 상태를 관리하는 방법도 엄청나게 다양합니다. 각각의 방법은 특정 방법을 해결하는데 초점을 맞추고 있으며 어떤 추상이든 이 문제에 따른 고통을 줄이기 위해서 애플리케이션에 복잡도를 더해야 합니다.

어떤 추상을 사용해야 하는가에 대한 핵심은 그 추상의 비용과 이점을 이해하는 데 있습니다. 이득이 무엇인지 알려면 그 추상이 풀기 위한 문제가 무엇인지, 그 문제를 해결하기 위한 다른 해법은 무엇인지 이해할 필요가 있습니다. 이 글에서는 제가 사용해보고 좋았던 추상도 공유하려고 합니다. 물론 여기서는 책이 아니기 때문에 모든 경우를 다 다루지 않고 기본적인 부분에 대해서만 설명합니다.

이 글에서는 애플리케이션의 규모와 복잡도가 성장할 때 추상을 추가하는 것을 어떻게 고려하게 되는지와 동일한 방법으로 접근합니다. 추상을 너무 이르게 추가하지 않는 것은 중요합니다. 필요보다 먼저 추가한 추상은 그 이득보다 더 많은 비용을 지불하게 될 수 있습니다.

그리고 여기서는 리액트에 특정한 해법을 다루지만 다른 프레임워크에서도 충분히 적용할 수 있는 일반적인 개념이 되었으면 합니다.

컴포넌트의 상태

먼저 컴포넌트의 상태부터 시작합니다. 상호작용이 필요한 애플리케이션이라면 어디선가 setState를 사용하게 될겁니다. 저는 생각보다 리액트 컴포넌트의 상태 API가 심할 정도로 인정을 덜 받고 덜 사용된다고 느낍니다. 믿을 수 없을 정도로 단순한 API며 이 API를 사용하더라도 애플리케이션에 그다지 복잡함을 더하지도 않습니다.

이 지식을 배우거나 다시 살펴보고 싶다면 다음 7분 분량의 (무료) 비디오를 확인해보세요. "리액트에서 컴포넌트 상태 사용하기". 그리고 공식 문서의 상태 들어 올리기를 확인해보세요.

이후 내용을 더 보기 전에 상태 API를 어디까지 사용할 수 있는지 보는 것을 진지하게 추천합니다. 이 API 자체만으로도 정말 강력하기 때문입니다.

컴포넌트의 상태를 사용하다가 문제가 생기기 시작하는 지점은 "프로퍼티 내리꽂기"에서 문제를 마주하게 될 때입니다. 프로퍼티 내리꽂기를 참고하세요. (역주: 번역)

자바스크립트 모듈 (싱글톤)

만약 싱글톤(singleton) 패턴에 익숙하지 않더라도 걱정하지 마세요. 자바스크립트에서는 상당히 직관적인 편입니다. 이는 단순히 모듈 안에 상태를 보관하는 패턴입니다. 이 패턴을 JS로 간단히 구현하면 다음과 같습니다.

const state = {}

const getState = () => state
const setState = newState => Object.assign(state, newState)

export {getState, setState}

"프로퍼티 내리꽂기 문제"에서 오는 불편함을 느끼기 시작할 때면 컴포넌트 상태 API에서 다음 단계로 넘어가는 것을 고려하게 됩니다. 리액트 애플리케이션이 성장한다는 것은 "컴포넌트 트리"도 함께 성장한다는 뜻입니다. 이 과정에서 애플리케이션에서 널리 공유되는 상태를 트리 위로 끌어 올리게 됩니다. 비대해진 컴포넌트 트리에서는 각 컴포넌트가 필요로 하는 상태와 상태를 갱신하기 위한 핸들러를 내려받는 과정이 정말 번거로워집니다.

그래서 일반적인 JS 모듈을 사용해 필요한 상태를 불러올 수 있다는 점에서 싱글톤 패턴은 꽤 괜찮은 선택지입니다. 그러니 프로퍼티를 내려 꽂는 것보다 컴포넌트 스스로 모듈을 불러와서 상태를 사용할 수 있게 됩니다.

물론 이 방법은 심각한 한계가 있습니다. 이 싱글톤에서 상태를 갱신할 때마다 컴포넌트가 다시 렌더링을 하도록 주의해야 합니다. 이 과정은 구현에 조금 더 노력을 필요로 합니다. (간단히 말해서 이벤트 에미터 (event emitter)를 구현할 필요가 있습니다.) 하지만 엄청 어려운 것은 아니며 이 과정을 도와줄 라이브러리도 존재합니다. 그러니 싱글톤 패턴을 사용하는 방법이 그렇게 나쁜 선택지는 아닙니다. 서버-사이드 렌더링을 필요로 하지 않는 경우라면 말이죠. (물론 대부분은 이 부분도 필요로 할 겁니다...)

컨텍스트 Context

리액트의 새 컨텍스트 API는 정말 멋집니다. 아직 이 기능을 모르신다면 리액트⚛️ 의 새 컨텍스트 API를 확인해보세요.

리액트 컨텍스트를 사용하면 프로퍼티 내리꽂기 문제와 싱글톤을 갱신하는 문제를 간단한 내장 API로 극복할 수 있게 됩니다. 이 API는 간단히 <ContextInstance.Provider /><ContextInstnace.Consumer /> 컴포넌트를 사용해서 어디서든지 상태에 접근할 수 있도록 구성할 수 있게 됩니다.

새로운 컨텍스트 API가 매우 단순한 덕분에 정말 많은 상황에서 단순히 싱글톤 패턴을 적용하는 것이 훨씬 간단해졌습니다. 리액트에서 컴포넌트 상태를 setState로 간단히 해결하는 것처럼 createContext를 사용해서 애플리케이션 상태를 해결할 수 있게 되었다는 점에서 정말 멋진 기능입니다.

Unstated

제임스 카일은 새 컨텍스트 API와 함께 사용할 수 있는, 새로운 상태 라이브러리를 만들었습니다. 이 새로운 라이브러리는 제가 상태를 공유할 일이 있는 앱이 있을 때 사용할 정도로 최애 라이브러리입니다. 이 라이브러리는 컨텍스트 위에 많은 것을 올리지 않는 조그마한 라이브러리라 좋습니다. 그리고 상태 컨테이너와 표현 컴포넌트를 명확하게 분리하고 있어서 모든 코드가 테스트하기 편리하니 한번 염두해보세요.

redux

Redux가 풀려고 의도했던 문제는 flux를 좀 더 손쉽게 사용할 수 있도록 하는 부분이었습니다. Flux는 상태의 흐름을 예측 가능하도록 만들어서 상태를 디버깅하는 방법을 개선하려고 했습니다. 또한 저장소에서 나온 데이터를 연산하는 과정을 단순화하려고 했습니다. 이 두 문제를 해결하기 위해서 명확한 관심사 분리와 단방향 데이터 흐름을 사용했습니다.

Redux는 이 과정을 정말 단순화해서 "flux 전쟁"을 2015년에 종식했습니다. 기존 Flux라면 필요했던 수많은 추상을 구현하지 않고도 flux의 이점을 누릴 수 있게 되었습니다. Redux 기반 상태 관리는 강점을 갖는 경우도 많고 사용 사례 또한 많습니다. 저는 이 라이브러리가 애플리케이션 상태 관리의 어려움을 해결했다는 점에 너무 기쁩니다. 이 라이브러리는 정말 많은 사람에게 도움이 되었습니다!

다시 말하면, 이 라이브러리를 사용한 앱이라면 엄청난 양의 복잡도를 더하게 되었다는 의미입니다. 이 라이브러리는 우회적인 계층을 애플리케이션에 추가하는 것으로 복잡한 사용자 상호작용을 구현하고 상태를 갱신했습니다. 이 라이브러리를 사용하는 프로젝트에 참여한 적이 있다면 디스패치와 액션, 크리에이터와 리듀서를 다 열어서 액션 타입명을 비교하는 일을 해봤을 겁니다. 그렇다면 복잡도에 대한 이야기도 이해할거라 생각합니다.

많은 애플리케이션이 redux를 사용하고 채택하는 이유는 react-redux의 connect 고차 함수가 프로퍼티 내리꽂기 문제를 우연히 해결했기 때문이라 봅니다. 프로퍼티 내리꽂기 문제는 상대적으로 작은 애플리케이션에서도 겪을 수 있는 고통인데 이 문제를 해결하기 위해 redux를 고려하는 경우가 많습니다. 더 쉬운 대안이 있다는 것을 모른 상태로 말이죠.

프로퍼티 내리꽂기 문제를 해결하기 위해 redux를 선택한다면 해결할 필요 없던 문제를 위해 비용을 지불하게 되는데 그로 인해 실제로 얻는 이득보다 더 많은 비용을 내게 됩니다.

그러니 다른 해결책을 먼저 사용해보세요. 그리고 상태를 필요로 하는 트리를 기준으로 redux에 보관하는 상태의 분량을 제한하세요. (일반적인 redux 사용자라면 최상위에 위치하고 있을 겁니다.)

코리 하우스가 이른 redux 적용에 대해 다음과 같은 트윗을 남겼습니다.

깨달음: Redux를 회사 프레임워크에 기본으로 넣은 것은 실수였다.

결과:

  1. 사람들이 모든 컴포넌트를 연결함.
  2. 사람들이 재사용 가능한 컴포넌트에도 Redux를 포함함.
  3. 모두가 Redux를 사용함. 심지어 필요가 없는 경우에도.
  4. 사람들이 React만 갖고 앱을 만들 줄 모름.

코리 하우스

결론

이 문제를 해결하기 위해 구현할 수 있는 수많은 추상과 패턴이 있지만 제가 이걸 다 적으려면 올해 내내 적어도 시간이 모자랄 겁니다. 😉

그래서 제가 강조하고 싶은 부분은 상태가 존재한다면 필요한 곳에 최대한 가까이 보관하라는 점입니다. 실무적인 용어로 설명하면 폼 입력창의 오류 상태를 전역 스토어에 보관하지 말라는 뜻입니다. 정말로 필요한 경우가 아니라면 말이죠. (그런 경우는 분명 드물껍니다.) 이 규칙을 따른다면 애플리케이션에서 컴포넌트 상태를 사용할 가능성이 매우 높다 는 뜻이고 이런 경우에 컨텍스트나 싱글톤을 애플리케이션 어딘가에서 아마도 사용하고 싶어질 겁니다. 컴포넌트 트리의 작은 일부 영역이라도 이 접근 방법은 매우 유용합니다.

컴포넌트를 여러 컴포넌트로 나눠야 할 때

어떤 상황에서 하나의 컴포넌트를 여러 컴포넌트로 나누는 게 맞을까요? 번역

2019년 3월 20일

이 포스트는 Kent C. DoddsWhen to break up a component into multiple components를 번역한 글입니다.

리액트 애플리케이션을 작성할 때 하나의 리액트 컴포넌트로 작성해도 된다는 점을 알고 있나요? 하나의 거대한 컴포넌트 안에 애플리케이션 전체를 넣는다고 해도 기술적으로 불가능한 것은 전혀 아닙니다. 거대한 render 메소드와 엄청 많은 인스턴스 메소드, 수많은 상태가 있을 것이고 아마도 모든 생명주기 훅(lifecycle hook)을 사용해야 할 겁니다. (shouldComponentUpdatecomponentWillUnmount는 예외겠네요. 항상 상태가 갱신된 데다 컴포넌트가 언마운트 될 일이 없으니까요!)

이 방법을 사용하면 다음 문제를 마주하게 됩니다.

  1. 성능이 저하될 수 있습니다. 상태 변화가 일어날 때마다 애플리케이션 전체를 새로 그리게 됩니다.
  2. 코드 공유와 재사용성이... 쉽지 않을 겁니다.
  3. 상태 관리도 어렵습니다. 어느 상태와 이벤트 핸들러가 어느 JSX 부분에 사용되는지 보다 보면 두통 😬이 올겁니다. 그리고 버그 🐜를 추적하는 일도 쉽지 않습니다. (이 부분은 관심사 분리의 장점 중 하나입니다.)
  4. 테스트는 100% 통합 테스트입니다. 물론 반드시 나쁜 것은 아닙니다. 하지만 엣지 케이스를 테스트하기 쉽지 않고 테스트를 하려는 부분을 격리하는 일이 어렵습니다. 그래서 이런 테스트를 유지하는 것 자체가 큰 도전입니다.
  5. 여러 엔지니어와 함께 이 코드 위에서 작업하는 일은 끔찍합니다. git diff의 결과가 어떨지, merge로 발생하는 코드 충돌이 상상이라도 가나요?!
  6. 서드파티 컴포넌트 라이브러리를 사용하는 게 불가능하지 않을까요? 단일 컴포넌트에 모든 것을 작성하는 것이 목표라면 서드파티 라이브러리 사용 자체가 좀 이상한 상황이 될 겁니다. 서드파티 컴포넌트를 사용한다고 하더라도 고차컴포넌트를 사용하는 react-emotion 같은 라이브러리는 어떨까요? 쓸 수 없겠죠!
  7. 선언적 컴포넌트 안에서 명령형 추상/API를 캡슐화하는 일은 어렵습니다. 한 거대한 컴포넌트 속 생명주기 훅에 널리 흩뿌려져 있게 되어 코드를 따라가기 더욱 어려워집니다.

이런 이유에서 컴포넌트로 나눠 작성합니다. 컴포넌트를 나누면 위 문제를 해결할 수 있게 됩니다.

한동안 AMA에서 앱을 컴포넌트로 나누는 좋은 방법/패턴이 있는가에 대한 질문을 받았습니다. 이게 제 답변입니다. "만약 위에서 살펴본 문제를 직면하게 될 때, 컴포넌트를 여러 작은 컴포넌트로 나눠야 할 때입니다. 미리 나누지 말고요." 단일 컴포넌트를 여러 컴포넌트로 나누는 일을 우리는 "추상(abstraction)"이라고 부르고 있습니다. 추상은 멋지지만 모든 추상은 비용이 따릅니다. 그래서 추상에 되려 당하기 전에 이 비용과 이득에 대해 유의하고 있어야 합니다.

중복은 잘못된 추상보다 훨씬 저렴합니다.Sandi Metz

저는 제 컴포넌트의 render 메소드가 정말 길어져도 크게 개의치 않습니다. JSX는 JavaScript 표현식의 모음이고 컴포넌트를 위해 선언적 API를 사용한다는 점을 기억하세요. 컴포넌트를 작은 크기로 나눠 이곳저곳에 프로퍼티 내리꽂기 (역자 주: 번역)를 하지 않고 render 메소드를 그대로 유지한다고 해서 그렇게 코드가 엄청 잘못되거나 하는 것은 아니라는 의미입니다.

결론

컴포넌트를 더 작은 크기로 쪼개는 것은 자유입니다. 하지만 진짜 문제를 경험하기 전까지는 크기가 커지는 컴포넌트에 대해 두려워하지 마세요. 너무 이르게 추상적으로 나누는 것보다 정말로 필요할 때까지 기다렸다가 적용하는 것이 코드 관리에 훨씬 편리합니다.

프로퍼티 내리꽂기 (prop drilling)

프로퍼티 내리꽂기가 무엇인지, 장단점은 무엇인지, 사용하면서 겪는 문제를 피하는 방법을 살펴봅니다 번역

2019년 3월 20일

이 포스트는 Kent C. DoddsProp Drilling을 번역한 글입니다.

이 글에서는 프로퍼티 내리꽂기(prop drilling)를 이해하는데 그치지 않고 어떤 부분이 문제가 될 수 있는지, 이 문제를 피하기 위해 사용할 수 있는 방법이 무엇인지 확인하려고 합니다. (혹자는 "나사 구멍 내기 threading" 라고도 합니다.)

프로퍼티 내리꽂기가 무엇인가요?

프로퍼티 내리꽂기(또는 나사 구멍 내기)는 리액트의 컴포넌트 트리에서 데이터를 전달하기 위해서 필요한 과정을 의미합니다. 다음 간단한 상태 컴포넌트 예제로 살펴보도록 합니다. (제가 가장 좋아하는 컴포넌트 예제입니다.)

class Toggle extends React.Component {
  state = {on: false}
  toggle = () => this.setState(({on}) => ({on: !on}))
  render() {
    return (
      <div>
        <div>The button is {on ? 'on' : 'off'}</div>
        <button onClick={this.toggle}>Toggle</button>
      </div>
    )
  }
}

이 컴포넌트를 둘로 리팩토링합니다.

class Toggle extends React.Component {
  state = {on: false}
  toggle = () => this.setState(({on}) => ({on: !on}))
  render() {
    return <Switch on={this.state.on} onToggle={this.toggle} />
  }
}

function Switch({on, onToggle}) {
  return (
    <div>
      <div>The button is {on ? 'on' : 'off'}</div>
      <button onClick={onToggle}>Toggle</button>
    </div>
  )
}

Switchtoggleon 상태(state)에 대한 참조가 필요하기 때문에 간단히 프로퍼티로 전달했습니다. 한번 더 리펙토링 과정을 거쳐 컴포넌트 트리에 하나의 레이어를 더 추가합니다.

class Toggle extends React.Component {
  state = {on: false}
  toggle = () => this.setState(({on}) => ({on: !on}))
  render() {
    return <Switch on={this.state.on} onToggle={this.toggle} />
  }
}

function Switch({on, onToggle}) {
  return (
    <div>
      <SwitchMessage on={on} />
      <SwitchButton onToggle={onToggle} />
    </div>
  )
}

function SwitchMessage({on}) {
  return <div>The button is {on ? 'on' : 'off'}</div>
}

function SwitchButton({onToggle}) {
  return <button onClick={onToggle}>Toggle</button>
}

이 과정을 프로퍼티 내리꽂기라고 합니다. on 상태와 toggle 핸들러를 필요한 위치에 두기 위해서 프로퍼티를 내려 꽂아(또는 나사 구멍을 내서) Switch 컴포넌트까지 전달해야 합니다. Switch 컴포넌트 자체는 동작을 위해 이 프로퍼티가 필요한 것이 아니더라도 자식 컴포넌트에 필요하기 때문에 프로퍼티로 받아서 전달해야 합니다.

왜 프로퍼티 내리꽂기가 좋은가요?

한번이라도 전역 변수를 사용하는 애플리케이션을 작업한 적이 있나요? AngularJS 애플리케이션에서 고립되지 않은 $scope를 사용하는 경우를 겪어본 적이 있나요? 많은 사람들이 이런 기법을 크게 거부하는 이유는 애플리케이션의 데이터 모델을 아주 혼란스러운 모습으로 이끌기 때문입니다. 누구든 데이터가 어디서 초기화되고 갱신되며 사용되는지 판단하기 쉽지 않은 상황에 마주하게 됩니다. 이런 상황에서 "이 코드를 수정하거나 지워도 다른 곳이 고장나지 않을까" 하는 질문에 답하기란 어렵습니다. 또한 이 질문은 당신이 작성한 코드를 최적화하는데 물어봐야 할 필수적인 질문입니다.

전역 변수보다 ES모듈을 선호하는 이유 중 하나는 이 모듈이 값이 어디서 사용되는지 더 명시적이며, 값을 추적하는데 더 쉽고, 코드 변경이 애플리케이션의 다른 영역에 어떤 영향을 주는지 파악하는데 용이하기 때문입니다.

가장 기초적인 수준의 프로퍼티 내리꽂기를 생각해보면 단순히 애플리케이션의 뷰에 어떤 값을 명시적으로 전달하는가에 해당합니다. 위 예제의 Toggle에서 on 상태를 열거형(enum)으로 리팩토링한다고 가정해보면 이 방식이 좋은 이유를 알 수 있습니다. 코드를 실행하지 않고도 정적으로 따라가는 것으로 어디서 사용하는지 쉽게 파악할 수 있고 수정도 간단하게 할 수 있습니다. 여기서 중요한 점은 암시적인 것보다 명시적으로 작성하는데 있습니다.

프로퍼티 내리꽂기로 발생하는 문제는 무엇인가요?

위에서 본 예제에서는 전혀 문제가 없습니다. 하지만 애플리케이션이 성장하면 여러 계층의 컴포넌트를 만들며 프로퍼티 내리꽂기를 하기 위해 이곳저곳에 깊은 구멍을 만드는 자신을 마주하게 될 겁니다. 일반적으로 작성하는 초기에는 큰 문제가 아닙니다. 코드를 계속 몇 주간 작성하다 보면 이런 과정이 거추장스럽게 느껴지는 몇 가지 경우를 겪게 됩니다.

  • 일부 데이터의 자료형을 바꾸게 되는 경우 (예: {user: {name: 'Joe West'}} -> {user: {firstName: 'Joe', lastName: 'West'}})
  • 필요보다 많은 프로퍼티를 전달하다가 컴포넌트를 분리하는 과정에서 필요 없는 프로퍼티가 계속 남는 경우
  • 필요보다 적은 프로퍼티를 전달하면서 동시에 defaultProps를 과용한 결과로 필요한 프로퍼티가 전달되지 않은 상황을 문제를 인지하지 어려운 경우 (또한 컴포넌트 분리 과정에서도)
  • 프로퍼티의 이름이 중간에서 변경되어서 값을 추적하는데 쉽지 않아지는 경우 (예를 들어 <Toggle on={this.state.on} />에서 <Switch toggleIsOn={on} />를 렌더링)

이외에도 프로퍼티 내리꽂기로 나타나는 여러 문제 상황이 있습니다. 특히 리펙토링을 진행하다보면 이런 부분에 고통을 겪습니다.

프로퍼티 내리꽂기에서 나타나는 문제를 어떻게 피할까요?

프로퍼티 내리꽂기에서 문제를 더 악화시키는 것 중 하나는 render 메소드를 불필요하게 여러 컴포넌트로 나누는 경우입니다. 큰 render 메소드 하나를 사용하는 것이 상황을 얼마나 쉽게 만들 수 있는가에 놀랄 겁니다. 가능한 한 메소드에 두세요. 이 메소드를 너무 성급하게 나눠야 할 필요가 없습니다. 다시 재사용해야 하는 상황이 될 때까지 기다린 후에 나누기 바랍니다. 그렇게 하면 불필요한 프로퍼티 내리꽂기를 할 필요가 없게 될겁니다.

재미있는 사실은 단 하나의 리액트 컴포넌트에 애플리케이션 전체를 작성한다고 한들 기술적으로 제약된 부분이 전혀 없습니다. 전체 애플리케이션의 상태를 관리하고 하나의 거대한 render 메소드를 사용하는 것도 가능합니다. 물론 이 방법을 권장하는 것은 아니지만요. 한번 생각해볼 만한 부분이라고 봅니다. :)

노트: 이 개념을 컴포넌트를 여러 컴포넌트로 나눠야 할 때 (역자 주: 번역)에서 작성했습니다. 관심있다면 확인해보세요.

프로퍼티 내리꽂기로 나타나는 문제를 완화하는 또 다른 방법은 defaultProps를 필수 프로퍼티에 사용하지 않는 방법입니다. defaultProps를 사용하면 컴포넌트가 제대로 동작하기 위해 실제로 필요한 프로퍼티를 전달받지 못한 상황인데도 중요한 오류가 숨겨지고 소리없이 실패하게 됩니다. 그렇기 때문에 defaultProps는 컴포넌트에 필수적이지 않은 부분에만 사용하기 바랍니다.

관련있는 상태는 될 수 있으면 가까이에 보관하기 바랍니다. 애플리케이션의 특정 부분에만 상태가 필요하다면 그 상태는 애플리케이션의 가장 높은 계층에 저장할 것이 아니라 최소 공통 부모 컴포넌트에서 관리해야 합니다. 상태 관리에 관해서는 애플리케이션 상태 관리 (역자 주: 번역)를 참고하기 바랍니다.

상태가 리액트 계층에서 정말로 깊숙이 위치한 경우라면 리액트의 새 컨텍스트 API를 사용하세요. 상태라고 애플리케이션 내 어디서나 다 접근할 필요는 없기 때문입니다. (컨텍스트 제공자는 어디서나 렌더링 할 수 있습니다.) 이 API는 프로퍼티 내리꽂기로 나타나는 몇 문제를 피하는 데 도움이 됩니다. 물론 이 컨텍스트는 전역 변수로 문제를 해결하는 방식과 유사성이 있습니다. 하지만 API가 디자인된 방식이 다르기 때문에 여전히 컨텍스트의 원천도 정적으로 찾을 수 있으며 어떻게 사용하든 상대적으로 손쉽게 사용 가능합니다.

결론

프로퍼티 내리꽂기는 장점도, 단점도 있는 양날의 검입니다. 위에서 언급한 올바른 사용법을 따르면 더 유지보수하기 쉬운 애플리케이션을 만드는데 도움되는 기능이 될 겁니다.

Tiny Tip Calculator 개발기

2019년 1월 22일

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로 설정한다.

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

독성 말투의 예시, 발생하는 커뮤니케이션 문제와 대안, 번역

2018년 12월 12일

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


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


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

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

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

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

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

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

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

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

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

거들먹거리는 말투

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

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

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

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

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

stackoverflow answer

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

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

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

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

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

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

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

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

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

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

평화적인 대안

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

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

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

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

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

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

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

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

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

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

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

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

기계적 말투

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

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

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

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

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

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

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

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

pet

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

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

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

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

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

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

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

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

평화적인 대안

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

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

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

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

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

저도 이 조언에 저항하는 데 참여할 수 있습니다. 제 경력 대부분 동안 이 조언에 저항했기 때문입니다. 어린 에이프릴은 "뭐, 사람들이 진실을 대하는 방법에 대해 배워야 할 뿐이라고" 같은 말을 했었습니다. 아마 제 그런 과거에서 저항하려는 이유가 있을 겁니다. 하지만 사람들과 강한 관계를 만들어가는 과정에서 서로 굳은 신뢰 관계(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: 지정지적으로 고쳤습니다. 아델라님 피드백 감사드립니다.