Skip to content

사내 신규 서비스 경험기

들어가며

2019년 9월부터 회사 내부에서 사용하는 신규 서비스를 담당하게 되었습니다. 서비스는 회사 내부에서만 사용되기 때문에 정식 명칭을 사용하지 않고, 신규 서비스라는 명칭을 사용하겠습니다. 이직 후 신규 서비스를 혼자 담당하는 것은 처음이라 지금까지 서비스를 운영하면서 쌓았던 경험에 신규 서비스를 하면서 쌓은 경험을 더하여 공유하고자 이 글을 정리하게 되었습니다.

정리한 글은 지금까지 제가 경험했던 내용을 정리한 내용이라 정답이 아닐 수 있습니다. 하지만 신규 서비스를 개발할 예정이거나 하는 분들에게 소소한 정보가 되었으면 합니다.

이 글의 순서는 이렇게 정리했습니다.

글의 순서

  • 꼼꼼하게 요구사항 분석 하기
  • 구현 전에 구조 설계하기
  • 코딩룰 정하기
  • 의미 있었던 코딩 가이드라인 5가지

꼼꼼하게 요구사항 분석 하기

왜 요구사항을 꼼꼼하게 분석해야 할까요

요구사항은 개발 초기에 반영되어야 만족하는 시스템을 개발할 수 있다. 요구사항이 누락되어 시스템 개발 과정에 도출된다면 전체 구조가 흔들릴 가능성이 매우 높다. 현실은 요구사항에 대한 누락이 자주 발생하고 관심이 부족하다. 아키텍처 설계를 제대로 하려면 요구사항 개발에 주목해야 한다.

  • 오병곤. 『실용주의 소프트웨어 개발』. 로드북, 2017.

이번에 느꼈던 서비스 운영과 신규 서비스 개발의 다른 점은 구현의 볼륨입니다. 신규 서비스는 밑바닥부터 모든 기능을 개발해야 하지만 서비스 운영 중에는 비교적 작은 기능 추가와 수정이 이뤄집니다. 신규 서비스는 그만큼 재사용을 위한 구현요소를 식별하는 작업 또는 협업자들과 논의해야 할 정책적인 요소가 많았습니다. 그래서 개발 초기에 요구사항을 꼼꼼하게 분석하고 기록하게 되었습니다.

분석된 요구사항은 개발을 위해서만 사용하는 게 아니라 개발 전에 협업자들과 협의할 주제가 되었습니다. 차트 라이브러리 관련 예를 들면,

Q. 프런트 엔드: 차트 라이브러리는 요구사항에 맞게 선정하면 될까요? 아니면 적용했으면 하는 게 있을까요?
  A.기획자: 기존에 Family Site에서 사용했던 차트 라이브러리 사용해주세요.
Q. 디자이너: 차트 라이브러리를 사용하면 어떤 부분을 커스텀 가능한가요?
  A.프런트 엔드: 라벨, 색상, 텍스트 크기 등이 가능합니다.
Q. 마크업: 차트 라이브러리를 사용하면 해당 영역은 어떻게 작업할까요?
  A.프런트 엔드: 영역만 잡아 주시면 해당 영역에 차트 라이브러리 적용하겠습니다.

개발 초기 단계에 정책이 수립된다면 개발 단계에서 구현에 집중할 수 있으므로 이러한 정책적인 부분을 개발 초기 단계에서 논의하고 결정하게 되었습니다.

그럼 어떻게 요구사항을 분석하고 기록했는지 설명하겠습니다.

요구사항 분석 방법

요구사항을 분석하는 방법은 저마다 다른 방법이 있다고 생각합니다. 제가 요구사항을 분석하는 방법은 기획서를 기반으로 사용자 스토리와 기능 리스트를 작성하는 것입니다. 여기서 사용자 스토리와 기능 리스트는 경험적인 내용을 정의한 부분입니다. 다른 정의를 알고 게시다면 다른 것이라고 구분해주시면 좋겠습니다.

먼저 사용자 스토리에 대한 설명하겠습니다.

사용자 스토리

사용자 스토리는 기획서에 표현된 기능을 사용자 입장에서 작성한 기능의 사용방법입니다. 사용자 입장에서 작성함으로써 보완이 필요한 기능이 있는지 식별할 수 있으므로 개발자 입장이 아닌 사용자 입장에서 서비스 사용방법을 작성합니다.

작성 형식은 ~하면 ~할 수 있다 와 같은 형식으로 언제, 무엇을 하는지 정의하며, 하나의 기능만을 표현합니다. 그리고 기획서를 왼쪽에서 오른쪽으로 위에서 아래 방향으로 보면서 각각의 사용자 스토리를 작성해야 합니다.

기획서 대신 사용자 스토리로 기능을 파악할 수 있으므로 기존에 있던 기능이라도 일단 작성하는 것을 권하고 싶습니다.

[예시 1.1] 사용자 스토리 예시

저는 메모 프로그램으로 리스트 형태로 작성합니다. 사용자 스토리를 구분할 수 있게 User Story(사용자 스토리)의 약자인 [US]를 사용합니다.

- [US] 결제하기 버튼을 클릭하면 결제 페이지로 이동할 수 있다.
- [US] 결제 페이지 접근 시 제품 이름, 가격, 배송지, 약관 동의하기 체크박스를 볼 수 있다.

사용자 스토리를 작성한 뒤에는 기능 리스트를 작성합니다.

기능 리스트

기능 리스트는 구현할 기능을 나열한 것입니다. 기능 리스트는 사용자 스토리를 기반으로 작성하고 하나의 사용자 스토리를 충족하기 위해 구현해야 할 기능을 모두 나열합니다. 작성 형식은 [담당] 작업 내용 형식으로 작성합니다.

저는 프런트 엔드를 담당하기 때문에 주로 백 엔드와 마크업 담당자와 협업을 진행합니다. 그래서 백 엔드와 협업이 필요한 부분은 [API]를 사용하고, 마크업 협업이 필요한 부분은 [마크업]으로 작성했습니다.

작성이 완료되면 공통된 기능을 그룹으로 만듭니다. 그룹을 만들게 되면 재사용 코드의 식별과 추상화 방법을 판단하기 용이하기 때문입니다.

[예시 1.2] 기능 리스트 예시
- [US] 결제하기 버튼을 클릭하면 결제 페이지로 이동할 수 있다.
  - [마크업] 결제 페이지 마크업
  - [프런트] 결제 페이지 마크업 반영
  - [프런트] 페이지 이동 링크 작업
- [US] 결제 페이지 접근 시 제품 이름, 가격, 배송지, 약관 동의하기 체크박스를 볼 수 있다.
  - [API] 제품 조회 API
  - [프런트] 제품 조회 API 연동
[예시 1.3] 공통 기능 리스트 예시

공통 기능 리스트를 작성하는 예시입니다. 아래에 보시는 것처럼 3번과 6번에서 공통 기능이 달력을 통한 날짜 선택임을 알 수 있습니다. 그래서 달력을 통한 날짜 선택 관련해서 공통 기능을 별도로 작성했습니다.

1. [US] 휴가 신청 메뉴 클릭 시 휴가 신청 페이지로 이동된다.
2. [US] 휴가 신청 페이지에서 달력, 사유 입력 양식을 볼 수 있다.
3. [US] 휴가 신청 페이지에서 달력을 통해 날짜를 선택할 수 있다.
4. [US] 리포트 메뉴 클릭 시 리포트 페이지로 이동된다.
5. [US] 리포트 페이지에서 달력, 차트를 볼 수 있다.
6. [US] 리포트 페이지에서 달력을 통해 날짜를 변경할 수 있다.
1. [공통] 달력을 통해 날짜를 선택할 수 있다.

다음으로는 구현 전에 구조 설계의 필요성과 사례에 대해 정리하였습니다.

구현 전에 구조 설계하기

왜 설계를 해야 할까요

아키텍처 설계는 개발 이전에 선행해서 진행해야 한다. 프로젝트 초기 단계부터 각 시스템 이해관계자와 긴밀한 소통을 통해 아키텍처 구조를 만들어가야 한다. 아키텍처 설계와 관련해서 가장 큰 이슈는 아직도 아키텍처 설계 필요성에 대한 인식과 아키텍처 설계 방법에 대한 이해가 부족하다는 것이다.

  • 오병곤. 『실용주의 소프트웨어 개발』. 로드북, 2017.

구현 중에 파일이나 코드 일부를 재사용할 필요가 생겨서 어떤 폴더에 그리고 어떤 형태로 해결해야 할지 고민하는 경우가 많이 있었습니다. 이러한 고민은 일과에 있어 적지 않는 시간이 소비되고 있음을 알 수 있었습니다. 그래서 저는 구현에 들어가기에 앞서 구조 설계를 진행하게 되었습니다. 여기서 말하는 구조 설계는 폴더 구조에 대한 설계를 의미합니다.

구조 설계의 목적은 구현에 투입되는 시간을 최소화하는 것입니다. 그리고 추후에 서비스 런칭 후에 유지 보수하는 데 투입되는 시간을 지속해서 낮게 유지하기 위해서입니다. 이번 주제에서는 어떤 사상과 원칙으로 설계를 진행했는지 설명하겠습니다.

사상과 관심 분리

사상

패러다임은 어떤 프로그래밍 구조를 사용할지, 그리고 언제 이 구조를 사용할지를 결정한다.

패러다임은 무엇을 해야 할지를 말하기보다는 무엇을 해서는 안 되는 지를 말해준다.

  • 로버트 C. 마틴. 『클린 아키텍처』. 송준이(역). 인사이트, 2019.

구조 설계에 전반적인 방향인 설계 사상을 담는 것이 일관성 있는 코드를 작성하는 것과 같은 갈래라고 생각합니다. 여러 명의 개발자와 함께 서비스를 개발할 때 코드에 일관성이 없다면 코드를 설명하는 시간과 코드를 이해하는 시간에 좀 더 많은 시간이 소비되었습니다. 그래서 이번에 신규 서비스의 구조 설계 시 첫 번째로 결정한 것은 프로그래밍 패러다임입니다.

프로그래밍 패러다임에 익숙하지 않은 분들을 위해 충분한 설명은 안 되겠지만 짧게 설명해 드리면, 프로그래밍 패러다임은 대중적으로 명령형, 객체지향, 함수형이 있습니다.

먼저 명령형은 작업 수행에 필요한 모든 단계를 노출하여 흐름이나 경로를 아주 자세히 작성합니다. 작업 수행에 필요한 단계의 예로는 루프, 분기, 값이 바뀌는 변수들이 있습니다.

[예시 2.1] 명령형 코드 예시
js
const todoList = []

addItem(todoList, '새해인사', 20200101)
addItem(todoList, '화분 물주기', 20200102)
addItem(todoList, '분리수거', 20200103)

printTodoList(todoList)

/* 실행결과
[20200101] 새해인사
[20200102] 화분 물주기
[20200103] 분리수거
*/
js
const addItem = (todoList, subject, date) => {
  const todoItem = toTodoItem(subject, date)
  todoList.push(todoItem)
}
const printTodoList = (todoList) => {
  const {length} = todoList
  let log = ``
  for (let i = 0; i < length; i++) {
    const todoItem = todoList[i]
    log += `${todoItemToString(todoItem)}`
    if (i < length - 1) {
      log += `\n`
    }
  }
  console.log(log)
}

const toTodoItem = (subject, date) => ({subject, date})
const todoItemToString = ({subject, date}) => `[${date}] ${subject}`

다음으로 객체지향은 상태와 행동을 지닌 자율적인 객체에 역할과 책임을 부여하고 다른 객체들과 협력하여 시스템을 구성합니다.

[예시 2.2] 객체지향 코드 예시
js
const todoList = new TodoList()
todoList.addItem('새해인사', 20200101)
todoList.addItem('화분 물주기', 20200102)
todoList.addItem('분리수거', 20200103)
todoList.print()

/* 실행결과
[20200101] 새해인사
[20200102] 화분 물주기
[20200103] 분리수거
*/
js
class TodoList {
  constructor() {
    this.todoList = []
  }
  addItem(subject, date) {
    const todoItem = new TodoItem(subject, date)
    this.todoList.push(todoItem)
  }
  print() {
    const str = this.todoList
      .map((todoItem) => todoItem.toString())
      .join('\n')
    console.log(str)
  }
}

class TodoItem {
  constructor(subject, date) {
    this.subject = subject
    this.date = date
  }
  toString() {
    return `[${this.date}] ${this.subject}`
  }
}

마지막으로 함수형은 가변(可變) 상태를 멀리하고 불변(不變) 상태를 추구합니다. 가변 상태는 조작의 타이밍이나 순서에 따라 예상과 다르게 동작할 위험이 있으므로 불변 상태를 추구합니다.

[예시 2.3] 함수형 코드 예시
js
const todoList = TodoList.create([])
  .addItem('새해인사', 20200101)
  .addItem('화분 물주기', 20200102)
const todoList2 = todoList
  .addItem('분리수거', 20200103)
js
console.group('todoList')
console.log(todoList.toString())
console.groupEnd()

console.group('todoList2')
console.log(todoList2.toString())
console.groupEnd()

/* 실행결과
todoList
  [20200101] 새해인사
  [20200102] 화분 물주기
todoList2
  [20200101] 새해인사
  [20200102] 화분 물주기
  [20200103] 분리수거
 */
js
class TodoList {
  constructor(todoList) {
    this.todoList = todoList
  }
  addItem(subject, date) {
    const todoItem = TodoItem.create(subject, date)
    const addedTodoList = this.todoList.concat(todoItem)
    return TodoList.create(addedTodoList)
  }
  toString() {
    return this.todoList
      .map(TodoItem.todoItemToString)
      .join('\n')
  }
  static create(todoList) {
    return new TodoList(todoList)
  }
}

class TodoItem {
  static create (subject, date) {
    return {subject, date}
  }
  static todoItemToString ({subject, date}) {
    return `[${date}] ${subject}`
  }
}

자바스크립트는 다중 패러다임으로 명령형, 객체지향, 함수형을 지원합니다. 많은 패러다임을 지원하기 때문에 개발자가 원하는 패러다임 관점으로 접근하여 요구사항을 구현할 수 있습니다. 같은 서비스에서 개발자마다 원하는 패러다임 관점으로 구현한다면 일관성이 부족한 코드를 구현하기 쉽다는 것은 경험으로써 알고 있었습니다. 그래서 다중 패러다임을 지원하는 자바스크립트 개발 환경에서 프로그래밍 패러다임의 결정은 중요하다고 생각합니다.

[사례 2.1] Nuxt.js 구조 설계 사례

Nuxt.js: Vue.js 어플리케이션을 만드는 프레임워크

Nuxt.js의 폴더 구조 기반과 함수형 사상을 반영하여 추가로 폴더 구조를 설계한 사례입니다. 항상 같은 결과를 표시하는 불변 컴포넌트와 가변 상태를 다루는 컴포넌트를 구분하고 순수 함수를 담을 수 있는 공간을 정의했습니다.

Nuxt.js에서 Vue 인스턴스에 독립적인 동작을 추가하는 공간을 plugins로 합니다. 여기서는 Vue 인스턴스에 추가하는 형태가 아닌 ES Module를 사용하는 방향으로 결정했습니다.

폴더 구조
├─ layouts
├─ pages
├─ components
│  ├─ pages
│  └─ <Feature>
└─ plugins
  • layouts: 가변 레이아웃 컴포넌트 폴더
  • pages: 가변 페이지 컴포넌트 폴더
  • components/pages: pages 폴더에 사용할 가변 컴포넌트 폴더
  • components/<Feature>: pages 폴더에 사용할 불변 컴포넌트 폴더
  • plugins: 독립적으로 사용 가능한 불변 로직 폴더

관심 분리

관심이란 소프트웨어의 기능이나 목적을 뜻한다. 관심을 분리한다는 것은 각각의 관심에 관련된 코드를 모아 독립된 모듈로 만들어 다른 코드로부터 분리한다는 뜻이다. 설계 기법에서 패턴의 대부분은 관심의 분리를 실현하려는 목표를 가지고 있다. 가장 대표적인 패턴이 MVC 패턴이다. MVC 패턴에서는 비즈니스 로직, 사용자에 대한 표시, 입력 처리를 분리한다.

  • 우에다 이사오. 『프로그래밍의 정석』. 류두진(역). 프리렉, 2017.

코드나 파일의 위치는 기능이나 목적별로 폴더라는 공간을 만들어주면 쉽게 결정이 가능한 부분이라고 생각합니다. 요구사항을 구현할 때 공통적인 기능을 추출하는 작업은 빈번하게 발생했습니다. 그때마다 코드나 파일의 위치를 의사 결정하는 것은 종합적으로 많은 시간을 소비하게 되었고, 미리 공간을 정해둠으로써 해결할 수 있었습니다.

기능이나 목적에 따라 관심을 분리하는 작업은 프로젝트를 세팅할 때마다 항상 새롭게 다가왔습니다. 그래서 지속해서 관리하고 누락을 방지하기 위해서 체크 리스트가 필요하다고 느꼈습니다. 이번 신규 서비스를 시작하기에 앞서 체크 리스트를 기반으로 폴더 구조 설계를 진행했습니다.

폴더 구조 체크 리스트

아래 항목들은 폴더 구조 체크 리스트입니다. 필수적으로 필요한 사항도 있고, 선택적인 사항이 있습니다. 프로젝트에 필요한 부분인데 누락된 부분이 있다면 추가하는 것을 권하고 싶습니다.

1. 컴파일되지 않은 에셋들을 포함하는 구성요소가 있는가?
2. 정적 파일들을 포함하는 구성요소가 있는가?
3. 페이지나 컴포넌트에 접근하기 전에 실행할 사용자 정의 함수를 정의하는 구성요소가 있는가?
4. 라우터를 정의하는 구성요소가 있는가?
5. 상태 관리를 하는 구성요소가 있는가?
6. 외부 라이브러리를 애플리케이션에 사용하도록 연결하는 구성요소가 있는가?
7. 상수를 정의하는 구성요소가 있는가?
8. 컴포넌트를 포함하는 구성요소가 있는가?
9. 컴포넌트 볼륨을 줄일 수 있는 구성요소가 있는가?
10. 페이지 단위를 분리할 수 있는 구성요소가 있는가?
11. 프로젝트 환경 설정을 포함하는 구성요소가 있는가?
12. 테스트 파일들을 포함하는 구성요소가 있는가?
[사례 2.2] Angular 기반 서비스에 체크 리스트 적용 사례

Angular: 하나의 프레임워크로 웹과 모바일을 동시에

이 사례는 오픈 빌더라는 서비스에 적용한 사례입니다. Angular의 폴더 구조 기반해서 폴더 구조를 추가하는 작업 했습니다.

항목 별 폴더
1. [assets] 컴파일되지 않은 에셋들을 포함하는 구성요소가 있는가?
2. [assets] 정적 파일들을 포함하는 구성요소가 있는가?
3. [guards] 페이지나 컴포넌트에 접근하기 전에 실행할 사용자 정의 함수를 정의하는 구성요소가 있는가?
4. [app.routing.ts] 라우터를 정의하는 구성요소가 있는가?
5. [states] 상태 관리를 하는 구성요소가 있는가?
6. [helpers] 외부 라이브러리를 애플리케이션에 사용하도록 연결하는 구성요소가 있는가?
7. [constants] 상수를 정의하는 구성요소가 있는가?
8. [shared/components, modules] 컴포넌트를 포함하는 구성요소가 있는가?
9. [pipes, directive] 컴포넌트 볼륨을 줄일 수 있는 구성요소가 있는가?
10. [feature.module.ts] 페이지 단위를 분리할 수 있는 구성요소가 있는가?
11. [src/environments] 프로젝트 환경 설정을 포함하는 구성요소가 있는가?
12. [e2e/src, *.spec.ts] 테스트 파일들을 포함하는 구성요소가 있는가?
폴더 구조
├─ src
│  ├─ assets
│  ├─ app
│  │  ├─ app.module.ts
│  │  ├─ app.routing.ts
│  │  ├─ constants
│  │  ├─ modules
│  │  │  └─ <feature>
│  │  │     ├─ feature.module.ts
│  │  │     ├─ feature.spec.ts
│  │  │     ├─ feature.page.ts
│  │  │     └─ feature.page.html
│  │  ├─ core
│  │  │  ├─ core.module.ts
│  │  │  ├─ apis
│  │  │  ├─ guards
│  │  │  ├─ helpers
│  │  │  └─ states
│  │  └─ shared
│  │     ├─ shared.module.ts
│  │     ├─ components
│  │     ├─ directives
│  │     └─ pipes
│  └─ environments
├─ e2e
│  └─ src

다음으로는 코드의 일관성을 높이기 위한 코딩룰 수립한 경험에 대해 정리하였습니다.

코딩룰 정하기

왜 코딩룰을 정해야 할까요

모든 프로그램은 순차 / 분기 / 반복이라는 세 가지 구조만으로도 표현할 수 있다.

  • 로버트 C. 마틴. 『클린 아키텍처』. 송준이(역). 인사이트, 2019.

코드를 작성하기에 앞서 구조 설계 시 프로그래밍 패러다임을 결정하였습니다. 프로그래밍 패러다임을 결정해도 실제로 코드에 반영할 방법은 무수히 많이 있습니다. 코드는 대부분 순차, 분기, 반복으로 이루어져 있고, 비교 연산, 구조 분해 할당과 같은 빈번하게 사용되는 요소들이 존재합니다. 여기서 분기를 작성하는 방법은 if, if/else, switch, 삼항 연산자 등과 같은 많은 방법이 존재합니다.

이러한 같은 목적과 의도를 가진 구현 요소들이 코드에 작성할 때마다 다르게 작성되어 있으면 일관성이 부족해지고 이해하기 힘들 거라 생각했습니다. 불규칙한 코드를 미리 방지하고자 구현 방법의 룰을 작성했습니다.

코딩룰 작성 사례

이번에 신규 서비스를 개발하며 작성했던 내용 일부를 발췌했습니다. 정답은 아닐 수 있지만 이러한 사례도 있다고 생각해주셨으면 좋겠습니다.

선언형 함수 사용

선언형의 표현이란 코드의 의도를 전하고자 할 때 가능한 명령형보다는 선언형으로 표현하는 것을 뜻한다. 명령형 프로그래밍은 문제의 해법, 즉 자료구조와 알고리즘을 기술한다. 반면에 선언형 프로그래밍은 문제의 정의, 즉 해결해야 할 문제의 성질이나 이때 충족해야 할 제약을 기술한다.

  • 우에다 이사오. 『프로그래밍의 정석』. 류두진(역). 프리렉, 2017.

선언형 함수는 해결할 문제를 기술한 함수입니다. 자바스크립트 반복문, 분기문 등 문법을 사용하여 로직을 기술하기보다는 함수명을 통해 로직을 기술합니다. 이번에 신규 서비스에 선언형 함수를 적극적으로 활용하면서 느꼈던 것은 선언형 함수는 중복 요소들을 식별하기 쉽고, 재사용성이 높으므로 선언형 함수를 사용하는 것을 선호하고 있습니다.

[예시 3.1] Array#map 함수

최근 들어 자주 사용하는 것은 Array의 메서드입니다. 그중에 map을 예로 들면 반복문으로 기술하던 이러한 형태의 명령형 코드를 선언형으로 대체할 수 있습니다.

js
// 명령형
const arr = [1, 2, 3]
for (let i = 0; i < arr.length; i++) {
  arr[i] = arr[i] * 10
}
console.log(arr)
// [10, 20, 30]

// 선언형
[1, 2, 3].map(v => v * 10)
// [10, 20, 30]

비동기 순차 처리

async function

비동기를 구현하는 방법은 콜백 패턴, Promise, async/await 등이 있습니다. 그중에 신규 서비스에서는 async/await 를 사용하여 동기식으로 기술합니다.

콜백 패턴은 비동기 흐름을 순차적이지 않은 방향으로 나타내며, 함수를 호출할 수 있는 권한을 넘겨줘야 하므로 선택하지 않았습니다. Promise는 사용하지만 async/await와 함께 사용하여 동기식으로 기술했습니다.

에러 처리는 try/catch를 사용하지 않고, then 또는 catch를 사용합니다. 함수 정의부에서 try/catch 를 사용하여 에러 처리의 예측이 어려운 경우가 생깁니다. 함수 정의부가 아닌 함수 사용 부에서 에러 처리를 하여, 에러 처리가 예측 가능한 게 이해하기 쉽다는 것을 경험했기 때문에 try/catch보다는 then 또는 catch로 처리하고 있습니다.

[예시 3.2] 비동기 순차 처리 예시
js
// 목업 함수
const backendApi = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 1000)
  })
}

// 함수 정의부
async function foo() {
  await backendApi()
  await backendApi()
  return 'ok'
}

// 함수 사용부
foo().then(console.log) // ok
[예시 3.3] 에러 처리 예시
js
// 목업 함수
const backendApi = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 1000)
  })
}
const errorApi = () => Promise.reject('에러 발생!')

// 함수 정의부
async function foo() {
  await backendApi()
  await errorApi()
  return 'ok'
}

// 함수 사용부
foo().catch(console.log) // 에러 발생!

반복

반복을 작성할 때는 for, while 등과 같은 문을 사용하지 않고, map, filter 등과 같은 선언형 함수를 사용합니다. 서비스에서는 Array의 메서드로 정의된 기능을 활용하고 있습니다.

[예시 3.4] 반복 사용 예시
js
const shoppingBasket = [
  { name: '물', price: 500, count: 3, checked: true },
  { name: '즉석밥', price: 1000, count: 2, checked: true },
  { name: '라면', price: 1000, count: 1, checked: true },
  { name: '귤', price: 10000, count: 1, checked: false },
  { name: '수세미', price: 2000, count: 1, checked: false }
]
 
const totalPrice = shoppingBasket
  .filter(({checked}) => checked)
  .map(({price, count}) => price * count)
  .reduce((totalPrice, producePrice) => totalPrice + producePrice)
 
console.log(totalPrice) // 4500

자료 구조 분해 및 할당

자료 구조를 분해 및 할당할 때는 구조 분해, 할당 문법을 사용하지 않고 선언형 함수를 사용합니다. 구조 분해는 pick, pluck 함수를 사용합니다. pick은 인자로 전달된 키만 추출하여 자료구조로 다시 만들어주는 함수입니다. pluck은 인자로 전달된 키의 값만 추출하여 배열로 전달해줍니다. 구조 할당은 assign 함수를 사용합니다. Object.assign를 감싼 assign 함수를 만들어 사용하는 것을 선호하고 있습니다.

[예시 3.5] 분해, 할당 함수 사용 예시
js
// 공통 함수 정의
const pick = (keys, obj) => keys
  .map(key => ({[key]: obj[key]}))
  .reduce((acc, obj) => Object.assign(acc, obj))
const pluck = (keys, obj)=> keys.map(key => obj[key])
const assign = (...objs) => Object.assign(...objs)

// 함수 사용
const obj = {key1: 'value1', key2: 'value2'}

pick(['key1'], obj) // { key1: 'value1' }
pluck(['key1'], obj) // ['value1']

assign({}, {key3: 'value3'}, obj)
// {key1: 'value1', key2: 'value2', key3: 'value3'}

마지막으로 코딩 시 유용했던 가이드라인에 대해 정리하였습니다.

의미 있었던 코딩 가이드라인 5가지

정도(程度) 지키기 위한 가이드라인

마지막 순서는 제가 선호하는 개발 원칙 다섯 가지에 대해 소개해 드리려고 합니다. 아무리 좋은 기술이나 설계가 있어도 정도(程度)를 지키는 게 중요하다고 생각합니다. 이 다섯 가지 원칙들은 개발의 정도(程度)를 지키는 데 많은 도움이 되었습니다. 사람마다 규모를 측정하는 범위가 다르고 쉽다고 느끼는 부분이 다르므로 구체적인 예시보다는 추상적인 가이드라인을 정리했습니다.

가이드라인 5가지

명명이 중요하다(Naming is important)

적절한 이름을 붙일 수 있었다는 것은 해당 요소가 바르게 이해되고 바르게 설계되어 있다는 뜻입니다. 반대로 어울리지 않는 이름을 붙여졌다는 것은 해당 요소가 달성해야 할 역할에 대해 본인이 충분히 이해하지 못했다는 뜻입니다.

이름은 코드를 통해 개발자끼리 의사소통을 이루어지므로 이름이 적절하지 않으면 코드상의 대화는 성립하지 않습니다.

[사례 4.1] 변수에 단위 표기

변수에 단위를 표기하지 않는 경우입니다. 작성자 본인이 아닌 경우 해당 변수의 값이 어떤 단위를 사용하는지 알기 힘든 경우를 빈번하게 봤습니다. 시간의 양이나 바이트의 수 값이 측정치를 포함한다면 단위를 포함하는 것이 좀 더 의도를 표현하기 좋았습니다.

js
// Not Cool
const start = new Date().getTime()
...
const end = new Date().getTime() - start
console.log(`Load time was: ${end} seconds`) // Wrong!!
 
// Cool
const startMs = new Date().getTime()
...
const endMs = new Date().getTime() - startMs
console.log(`Load time was: ${endMs / 1000} seconds`)

KISS(Keep It Short and Simple)

코드를 작성할 때는 최우선 가치를 단순성과 간결성에 둡니다. 복잡한 코드는 읽기 어렵고 수정하기 어려워집니다. 프로그래밍 중에 코드가 동작할 수 있는 가장 간단한 방법은 무엇인지 항상 질문을 던져야 합니다.

[사례 4.2] reduce 사용 사례

이 사례는 코드리뷰에 리뷰이로 있을 때 코멘트 받은 부분을 발췌했습니다. Array 메서드인 reduce의 두 번째 인자를 활용하여 KISS를 반영한 사례입니다.

js
// AS IS
const toTotalPrice = (prices) => {
  if (prices.length) {
    return prices.reduce((totalPrice, price) => totalPrice + price)
  } else {
    return 0
  }
}
 
toTotalPrice([1000, 500, 1500]) // 3000
toTotalPrice([]) // 0

// TO BE
const toTotalPrice = (prices) => {
  return prices.reduce(
    (totalPrice, price) => totalPrice + price,
    0
  )
}
 
toTotalPrice([1000, 500, 1500]) // 3000
toTotalPrice([]) // 0

DRY(Don't Repeat Yourself)

똑같은 코드가 여러 군데 있으면 모든 곳을 정확하게 수정하지 않는 이상 전체적으로 정합성을 보장하기 힘듭니다. 중복된 숫자, 문자, 옵션값을 상수로 정의하고, 중복된 로직은 함수로 정의하고, 중복된 로직과 상태는 모듈화로 정의할 것을 권하고 싶습니다.

해결책에 대한 사고의 중복은 디자인 패턴을 통해 해결합니다. 같은 문제에 관해 반복해서 해결책을 생각하는 중복을 일어나지 않게 하는 기법의 하나가 디자인 패턴입니다.

SLAP(Single Level of Abstraction Principle)

코드를 작성할 때 높은 수준의 추상화 개념과 낮은 수준의 추상화 개념을 분리하여 하나의 수준만 추상화하도록 하는 원칙입니다. 추상화 단계는 상하가 아니라 기능의 복잡도에 따라 여러 계층으로 분리합니다. 결과적으로 추상화 수준을 일치시킨 코드는 훌륭한 책과 같습니다. 최고 수준부터 중간 수준의 처리가 책의 목차가 되고 최저 수준의 처리가 책의 본문 내용이 됩니다.

function 고수준() { 중수준1(); 중수준2(); } // 수준1의 목차
function 중수준1() { 저수준1(); 저수준2(); } // 수준2의 목자-1
function 저수준1() { }
function 저수준2() { }
function 중수준2() { 저수준3(); }
function 저수준3() { }

YAGNI(You Aren't Going to Need it)

확장성을 고려해서 넣은 설계라도 예상은 대부분 빗나가는 경우가 많습니다. 빗나간다는 것은 거기에 들인 시간이 쓸모없어진다는 뜻이기도 합니다. 범용성이 가져다주는 재사용성이나 확장성도 좋지만, 그보다는 우선 사용할 수 있는 데 가치를 두는 단순성을 생각하는 것을 권하고 싶습니다.

마치며

지금까지 전달 드렸던 소소한 팁을 요약하면 아래와 같습니다.

  • 요구사항은 초기에 꼼꼼하게 분석할 필요가 있으며, 신규 서비스에서는 사용자 스토리와 기능 리스트를 통해 해결함
  • 구현 전에 구조 설계를 진행할 필요가 있으며, 신규 서비스에서는 프로그래밍 패러다임과 관심 분리를 통해 해결함
  • 전체적인 코드의 통일성을 위해 구현 규칙이 필요하다고 생각함
  • 구현은 정도(程度)를 지켜 단순하고 간결하게 하는 것이 좋다고 생각하며, 현재 필요한 부분만 구현해야 함

신규 서비스를 개발할 때 더욱 도움이 되겠지만 어떠한 부분은 프로젝트를 운영하며 적용할 수 있는 정보라고 생각합니다. 지금까지 제 나름대로 세운 기준으로 경험기를 정리했습니다. 이 기준에 살을 덧붙여서 좋은 지식이 되었으면 좋겠습니다.

지금까지 읽어주셔서 감사합니다.