이 글은 에이전틱 코딩에서의 80% 문제에서 알게 된 내용을 기록한 글이다.

자신의 역할 구현자에서 오케스트레이터(관리자)로 재정의해야 한다.

이제 자신의 일이 한 줄 한 줄 코드를 작성하는 것이 아니라, 아키텍처를 감독하고 품질을 통제하는 것임을 받아들여야 한다.

 

프롬프트는 명령형이 아닌 선언형으로 작성해야 한다.

  • 기존 모델(명령형): “X를 받아 Y를 반환하는 함수를 작성해. 이 라이브러리 사용하고. 이 에지 케이스 처리해. 그리고…”
  • 새로운 모델(선언형): "요구사항은 이거고, 통과해야 할 테스트는 이거고, 성공 기준은 이거야. 방법은 네가 찾아."

권장 패턴

AI에게 무엇을 할지 일일이 지시하지 마세요. 대신 성공 기준을 주고 기준을 만족할 때까지 반복시키세요. 마법은 에이전트의 코드 "작성"에 있는 게 아니라, 여러분이 정한 조건을 만족할 때까지 반복하는 데 있습니다.
  • 먼저 테스트를 쓰고, 에이전트가 통과할 때까지 반복시킨다
  • MCP로 브라우저에 붙여서 동작을 시각적으로 검증하게 한다.
  • 단순하지만 올바른 버전을 먼저 구현한 뒤, 정확성을 유지하면서 최적화한다.
  • API 규격을 정의하고, 스펙에 맞게 구현하게 한다.

단, 이 모든 것은 성공 기준이 실제로 올바를 때만 통한다. 입력이 잘못되면 출력도 잘못된다. 그리고 모델의 능력이 높아질수록, 그 잘못은 더 크게 증폭된다.

이 접근으로 성공하는 개발자는 시간의 70%를 문제 정의와 검증 전략에 쓰고, 30%만 실행에 쓴다.

효과적으로 프롬프트를 작성하는 방법을 꾸준히 배워야 한다.

품질을 올리기 위한 방법

  • 새로운 콘텍스트에서의 코드 리뷰: 깨끗한 상태에서 다시 읽게 하면, 스스로의 실수를 꽤 잘 찾아낸다.
  • 모든 단계에서의 자동 검증: CI/CD, 린터, 타입 체커, 테스트를 가드레일로 사용한다.
  • 에이전트 자율성에 대한 의도적인 제약: 작업 범위를 한정하고, 성공 기준을 명확히 한다.
  • 중요한 결정에 사람이 반드시 개입하는 구조: 구조의 결정은 여전히 사람이 중심에 있어야 한다.

그러면서도 자신의 성장을 제한하는 방식으로 AI에 의존하면 안 된다.

에이전트를 과도하게 쓰는 사용자에게 기술 퇴화의 초기 징후가 나타나고 있다. 이를 해결하기 위한 답은 없지만, 필자는 이런 걸 해보고 있다고 한다:

  • TDD(테스트 주도 개발): AI가 구현하게 하기 전에 테스트(또는 테스트 케이스)를 쓰거나 생각합니다.
  • 시니어와 페어링: AI 제안을 실시간으로 논의해서 의사결정 과정을 배웁니다.
  • 설명 요청: AI가 해결책만 만들지 말고 접근을 정당화하게 합니다.
  • 번갈아 하기: 감각을 유지하기 위해 일부 기능은 직접 구현합니다.

'AI' 카테고리의 다른 글

프로그래머를 위한 프롬프트 엔지니어링 플레이북  (3) 2025.06.30
CNN  (0) 2022.07.22
원문 : https://www.anthropic.com/engineering/code-execution-with-mcp
참고 : 기존 `@modelcontextprotocol/sdk` 기반으로 구축한 MCP 서버는 "도구 사용" 기반의 아키텍쳐를 사용한다. 만약 "코드 실행" 기반의 아키텍쳐로 변경하려면 기존 라이브러리를 사용하지 않고, 해당 기반 환경을 직접 구축하거나 관련 기능을 지원하는 새로운 라이브러리를 찾아야 한다.

기존 MCP "도구 사용(Tool-use)" 방식의 한계

MCP client는 Model의 컨텍스트 윈도우에 도구 정의를 불러온다. 그리고 각 도구 호출과 결과가 작업 간 모델을 통과하는 메시지 루프를 조율한다.

 

1. 도구 정의가 컨텍스트 윈도우를 과부하시킨다

컨텍스트 윈도우(context window) : LLM이 한 번에 입력으로 받아들일 수 있는 최대 토큰 길이

대부분의 MCP client는 모든 도구 정의를 처음부터 컨텍스트에 로드해서 직접적인 도구 호출 문법을 사용해 모델에 노출시킨다.

도구 설명이 길수록 더 많은 컨텍스트 윈도우 공간을 차지하여 응답 시간과 비용을 증가시킨다.

// 도구 정의 예
gdrive.getDocument
     Description: Retrieves a document from Google Drive
     Parameters:
                documentId (required, string): The ID of the document to retrieve
                fields (optional, string): Specific fields to return
     Returns: Document object with title, body content, metadata, permissions, etc.

 

2. 중간 도구 결과는 추가 토큰을 소모한다

예를 들어, 에어전트에게 "Google Drivie에서 제 회의 대본을 다운로드해서 Salesforce 리드에 첨부해주세요"라고 요청할 수 있다. 이때 모델은 다음과 같이 호출한다: `gdrive.getDocument` 호출 --(중간)--> `salesforce.updateRecord` 호출

 

모든 중간 결과는 반드시 모델을 통과해야한다. 따라서 이 예시에서는 대본 내용이 두 번 모델에 전달된다. 이는 더 큰 문서일 경우 컨텍스트 윈도우 제한을 초래할 수 있다.

 

해결책 : LLM이 직접 코드를 작성("Code-Execution")

장점

  • 컨텍스트 효율성: 모든 도구 명세를 미리 로드할 필요 없이, 파일 시스템에서 필요한 모듈만 import하여 사용하므로 컨텍스트를 극적으로 절약할 수 있다.
  • 강력한 제어 흐름: 코드를 사용하므로 반복(loops), 조건(conditionals), 예외 처리(error handling) 등 기존 방식으로는 불가능했던 복잡한 로직 구현이 가능하다.
  • 인메모리 데이터 변환: 대용량 데이터를 샌드박스 내에서 직접 가공/필터링하여 꼭 필요한 최종 결과만 LLM에 반환할 수 있어 효율적이다.
  • 프라이버시: 민감한 데이터를 샌드박스 내에서 처리하고, 그 결과를 LLM에 노출하지 않고 다음 행동을 결정할 수 있다.
  • 상태 저장(Statefulness): 실행 환경의 파일 시스템을 활용하여 여러 차례의 대화에 걸쳐 상태나 데이터를 유지할 수 있다.

단점

  • 복잡한 인프라 요구 : 에이전트가 생성한 코드를 실행하기 위해 적절한 샌드박스, 자원 제한, 모니터링이 포함된 안전한 실행환경이 필요하다.

 

사용 예정

web-codegen-scorer : LLM에서 생성된 코드 품질 측정기,  에 초점을 맞춰 평가하는 게 큰 특징

  • AI로 생성된 코드에 대해 증거 기반 결정을 내릴 수 있다:
    • 프로젝트에 가장 효과적인 지침을 찾을 수 있다.(스코어를 기반으로 미세조정)
    • 다른 모델에서 생선된 코드의 품질을 비교할 수 있다.
  • 만든이 : Google의 Angular 팀
  •  

 

참고

Slim Select 3.0: 고급 선택 드롭다운 컨트롤 — 종속성이 없는 기능이 풍부한 선택 드롭다운 구성 요소입니다. v3.0은 공식 React 구성 요소 옵션을 추가하고, 버그를 수정하고, 접근성을 개선 합니다.

  • 특징
    • 가볍고 빠름 : KB 단위의 용량으로 탁월한 성능 제공
    • 접근성 우선 : 완전한 ARIA 지원
    • 광범위한 커스터마이징 지원 : css/scss 변수, JS 구성을 사용해 사용자 정의 가능
    • 개발자 친화적 : 깔끔한 문서, TS 지원, 깔끔한 API
  • 프레임 워크 지원 : Vue, React
  • 기능
    • 단일/다중 선택 지원
    • 옵션 그룹 지원 : 하나의 드롭다운 메뉴에서 카테고리로 구분해서 표시하는 형태(하위 메뉴가 펼쳐지는 구조X)

배운 기술 다시 보기

파트 1 : 액션과 계산, 데이터

  • 코드에서 액션과 계산, 데이터를 구분해 가장 문제가 되는 부분을 찾는다.
  • 액션에서 계산을 빼내 재사용하기 좋고 테스트하기 쉬운 코드를 만든다.
  • 암묵적인 입력과 출력을 명시적인 것으로 바꿔 액션의 설계를 개선한다.
  • 불변성을 구현해 데이터 읽기를 계산으로 만든다.
  • 계층형 설계로 코드를 구성하고 개선한다.

파트 2: 일급 추상

  • 언어의 문법을 일급으로 만들어 코드를 추상화할 수 있다.
  • 함수형 반복/도구를 이용해 고차원으로 추론한다.
  • 함수형 도구를 연결해 데이터 변환 파이프라인을 만든다.
  • 타임라인 다이어그램을 통해 동시성 분산 시스템을 이해하고, 버그를 찾고 수정한다.
  • 고차 함수로 안전하게 상태를 변경한다.
  • 반응형 아키텍처를 사용해 원인과 효과의 결합을 줄인다.
  • 세상과 상호작용하기 위해 어니언 아키텍처 설계를 서비스에 적용한다.

꼭 기억해야 할 것

액션에 계산이 숨어있을 수 있다

  • 계산을 찾아서 빼는 것은 가치 있는 일이다.
    • 계산은 액션보다 재사용하기 좋고, 테스트하거나 이해하기 쉽다.
  • 액션 / 계산 / 데이터는 변경되는 빈도에 따라 각각 다른 계층으로 구성할 수 있다. 결국 넓은 관점으로 어니언 아키텍처로 구성하게 된다.

고차 함수를 사용하면 추상화에 대한 개념이 넓어진다

  • 고차 함수를 사용하면 똑같은 저수준의 코드를 반복적으로 만들지 않아도 된다.
  • 그리고 도메인에 집중할 수 있다.

코드에서 시간의 의미는 마음대로 바꿀 수 있다

  • 액션은 실행 시점(순서)과 실행 횟수(반복)가 중요하다.
  • 코드가 여러 타임라인에서 실행된다면 기대하지 않은 실행 순서 때문에 버그가 생길 수 있다. 타임라인 다이어그램으로 시각화하여 액션 실행(동시에 실행 or 차례로 실행 등)을 확인해서 버그를 찾고 수정할 수 있다.
    • 함수형 개발자는 액션의 순서와 반복을 바꿀 수 있다. 기본형을 활용하면 된다.(`Cut()`, `JustOnce()`, `ValueCell()`,...)

투 트랙으로 마스터되기

기술을 처음 배웠을 때는 열정이 넘치는 것에 비해 숙련도가 낮다. 그래서 기술을 적용하지 않아야 할 곳에 적용하는 위험이 있다. 열정이 가장 높을 때 위험을 극복하면서 기술을 연습할 수 있도록 투 트랙 모델을 제안한다.

 

트랙 1: 샌드박스

기술을 연습하고 실험할 공간이 필요하다.

  • 제품 코드에 버려진 브랜치
  • 사이드 프로젝트
  • 연습 문제로 연습

 

사이드 프로젝트

실패에 대한 큰 위험 없이 즐겁게 연습하기 좋다.

 

처음에는 작은 프로젝트로 시작해라

  • 큰 프로젝트는 성취감을 느낄 수 없으므로 크기를 작게 유지해라. 기술이 익숙해지면 프로젝트도 키울 수 있다.
  • 스스로에게 물어보세요.
    • ~~ 앱과 비슷한 것이 무엇인가요?

재미로 하는 사이드 프로젝트를 만들어라

  • 부담 없이 할 수 있다.
  • 스스로에게 물어보세요.
    • 일 같지 않으면서 엉뚱한 것은 무엇인가요?
    • 실패해도 재미있는 것은 무엇인가요?

익숙한 기술과 새로운 기술을 함께 사용해라

  • 자신 있는 기술을 사용해 새 기술을 적용해 보세요.
  • 스스로에게 물어보세요.
    • 바로 만들 수 있는 것은 무엇인가요?
    • 어떻게 하면 그 위에 새로운 기술 하나를 적용할 수 있을까요?

언제든지 기능을 추가해 볼 수 있는 프로젝트를 준비해라

  •  준비되어 있는 사이드 프로젝트가 있으면 좋다. 이를 기반으로 새로운 기술을 추가하면 된다.
  • 스스로에게 물어보세요.
    • 어떤 프로젝트가 언제든지 기능을 추가할 수 있는 기반이 될까요?
    • 나중에 추가할 만한 기본 기능은 무엇인가요?

연습 문제로 훈련하기

연습 문제는 맥락이 필요 없고 명확한 요구 사항이 있다. 훈련을 위해 만들었기 때문에 성취할 결과물 없이 기술을 연마할 수 있다.

관련 사이트

  • Edabit(https://edabit.com/challenges), CodeWars(https://www.codewars.com/)
    • 문제가 작고 명확하게 설명되어 있다. 함수형 프로그래밍 기술을 연습해 봐라.(같은 문제에 다른 기술을 적용해서 풀 수도 있다.)
  • Project Euler(https://projecteuler.net/)
    • 수학적 문제도 섞여 있다. 이 사이트의 가장 좋은 점은 메모리 제한/성능 제한/스택 크기 제한 등의 다양한 제약이 있는 것이다. 이는 기술을 실용적으로 사용하는 데 도움이 된다.
  • Code Katas(https://github.com/gamontal/awesome-katas)
    • 같은 문제를 반복적으로 풀어보는 방법의 훈련을 할 수 있다. 문제를 푸는 것보다 프로그래밍하는 과정을 연습하기 위해 사용한다.

트랙 2: 제품

냉정하게 기술이 익숙해진 것 같다는 생각이 들면 실제 프로젝트에 적용해 볼 수 있다. 다음은 제품에 적용해 볼 수 있는 방법이다.

  • 이미 있는 코드를 리팩터링
  • 이미 있는 프로젝트에 새로운 기능
  • 새로 만드는 깨끗한 제품
  • 다른 사람들을 가르치기

오늘 당장 버그를 없애 보세요

규모가 작든 크든 버그를 개선할 수 있다.

 

변경 가능한 전역변수를 하나씩 줄인다

  • 전역변수는 공유할 수 있는 데이터다. 변경 가능한 데이터를 공유하는 곳은 버그의 진원지가 될 수 있다. 암묵적 입력과 출력의 대상(3~5장)이 변경 가능한 전역 변수이면 수정하자.
  • 변경 가능한 전역변수를 하나씩 줄여보자.
  • 전역변수 목록을 만들고 하나를 선택해 리팩터링으로 없애보자.

타임라인을 하나씩 줄인다

  • 타임라인 다이어그램으로 경쟁 조건과 순서에 관한 문제를 알 수 있다.
  • 타임라인을 격리하고 공유하고 조율하면 기대하지 않는 실행 순서를 없앨 수 있다.
  • 긴 코드 베이스에서 경쟁 조건이 있다면? 하나를 선택하여 잘못된 순서를 없애는 기술을 사용해 타임라인 다이어그램을 하나씩 줄여보자.

설계를 하나씩 개선하세요

이 책에서 배운 기술 중 어떤 것은 코드 설계를 점진적으로 개선하기 위해 바로 사용할 수 있다.

 

액션에서 계산을 빼자

  • 대부분의 액션은 목적이 있기 때문에 없애는 것은 어렵다.
  • 대신 액션을 작게 만들자. 액션에서 로직을 찾고 로직을 계산으로 빼내자.

암묵적 입력과 출력을 명시적으로 바꿔라

  • 암묵적 입력과 출력이 줄어들면 동일한 액션이어도 더 좋은 액션이 된다. 시스템의 상태와 느슨하게 결합하기 때문이다.

반복문을 보자

  • 반복문 대신 `forEach()`, `map()`, `filter()`, `reduce()`를 사용하자.
    • 함수형 스타일로 조금씩 다가가자.

더 읽을거리

다음 단계로 넘어가기 위해 읽어보는 좋을 책을 추천한다

 

<Functional-Light JavaScript> - Kyle Simpson

  • 자바스크립트 스타일 가이드와 함수형 프로그래밍에서 사용하는 용어들을 함께 설명한다.
  • 함수형 프로그래밍 개념들을 잘 설명하고 있어서 강력하게 추천한다.

<Domain Modeling Made Functional> - Scott Wlaschin

  • 고객의 요구사항을 함수형 작업 흐름으로 구현하는 방법을 보여준다.
  • 타입 시스템을 사용해서 도메인을 모델링한다.(필자가 본 DDD(Domain Driven Design) 책 중 최고이다)

<Structure and Interpretation of Computer Programs> - Harold Abelson, Gerald Jay Sussman, Julie Sussman*

  • 고전 책이다.(MIT 교재)
  • 내용이 어렵지만 중요한 개념들을 배울 수 있다.
  • 함수형 프로그래밍 기술을 학습하면서 항상 옆에 두고 보는 책이라고 생각해라.

<Grokking Functional Programming> - Michat Prachta

  • 다른 관점에서 함수형 프로그래밍을 소개한다.
    • 순수 함수, 함수형 도구, 불변 데이터에 대해 더 깊이 이해할 수 있다.
    • 데이터 모델링에 대해 더 자세히 다룬다.

두 아키텍처 패턴은 독립적이다.

반응형 아키텍처와 사용하면 반응형 아키텍처가 어니언 아키텍처 안에 들어 있는 것을 볼 수 있지만 서로 의존하진 않는다.

반응형 아키텍처

반응형 아키텍처는 앱을 구조화하는 방법이다.

핵심 원칙은 이벤트에 대한 반응으로 일어날 일을 지정하는 것이다. 원래는 핸들러 함수에서 순서대로 실행되던 것들을 여러 개의 핸들러에서 실행되도록 나눈다.

  • X가 일어나면 Y, Z, A, B, C를 순서대로 처리한다고 할 때 다음과 같이 처리한다.
    • X가 일어나면 Y를 한다.
    • Y가 일어나면 Z를 한다.
    • (... 동일한 방식으로 나머지 일도 처리한다.)

예: 웹 서비스 / 사용자 인터페이스

이 아키텍처는 웹 서비스와 UI에 잘 어울린다.

웹 서비스 적용 전 웹 서비스 적용 후 사용자 인터페이스 적용 전 사용자 인터페이스 적용 후

 

코드에 나타난 순차적 액션의 순서를 뒤집는다. 이러면 코드를 읽기 쉽고, 유지보수하기 좋다.

  • 변경 전 : X를 하고 Y를 실행한다.
  • 변경 후 : X가 일어나면 언제나 Y를 실행한다.

하지만 항상 좋은 것은 아니다. 언제 사용하고 어떻게 사용할지 잘 판단해서 사용해야 한다.

ex) 장바구니 예제

일급 상태 만들어주는 `ValueCell()` 함수를 만들어 적용하기

장바구니 예제에서는 전역 상태는 장바구니뿐이다. 장바구니 상태를 일급 함수로 만들어보자.

 

`ValueCell()` : 변경 가능한 값을 일급 함수로 만드는 함수

function ValueCell (initialValue) {
  var currentValue = initialValue;	// 외부에서 직접적으로 변경 불가능한 값
  return {
    val: function() {
      return currentValue;			// 값 읽기
    },
    update: function(f) {			// 값 교체
      var oldValue = currentValue;
      var newValue = f(oldValue);	// 함수를 적용해 값을 바꾼다
      currentValue = newValue;
    }
  };
}

 

위 코드를 장바구니에 다음과 같이 적용한다. 이제 `shopping_cart`를 읽고 쓰는 코드는 명확한 메서드 호출로 바꿨다.

일급 상태를 만들어 주는 함수를 반응형으로 만들기

`ValueCell`코드에 감시자(watcher) 개념을 추가해 보자. 감시자로 장바구니가 바뀔 때 할 일을 지정(핸들러)할 수 있다.

 

감시자 개념은 다른 이름으로 사용하기도 한다 : 이벤트 핸들러, 옵저버, 콜백, 리스너

셀이 바뀔 때 배송 아이콘을 갱신하도록 수정하기

장바구니가 바뀔 때 항상 배송 아이콘이 갱신되어야 한다. `shipping_cart` 값을 가진 `ValueCell`에 `update_shipping_icons()`를 감시자로 등록하자.

 

결과

  • 핸들러 함수가 더 작아졌다.
    • 아이콘 갱신을 직접 하지 않는다. 이 책임은 감시자로 이동했다.
  • 장바구니를 바꾸는 모든 핸들러에서 `update_shipping_icons`을 부르지 않아도 되며, 장바구니가 변경될 때마다 항상 실행된다.
    • 따라서 장바구니에 제품 추가, 삭제, 수량 변경 등을 할 때마다 일일이 호출하지 않아도 된다.

기존 셀에서 파생된 값을 계산하는 `FormulaCell` 함수 만들기

어떤 셀은 다른 셀의 값을 최신으로 반영하기 위해 파생될 수 있다.

`FormulaCell`로 이미 있는 셀에서 파생한 셀을 만들 수 있다.

  • `FormulaCell`은 값을 직접 바꿀 수 없다. 감시하던 상위(upstream) 셀 값이 바뀌면`FormulaCell` 값이 바뀐다.
    상위 셀 값이 바뀌면 상위 값을 가지고 셀 값을 다시 계산한다.

장바구니 값이 바뀔 때 `total` 값을 재계산하도록 수정하기

`FormulaCell`을 감시할 수 있기 때문에 `FormulaCell`이 `total`값이라면 `total`값이 바뀔 때 실행할 액션을 추가할 수 있다.

결과 검토

함수형 프로그래밍과 변경 가능한 상태

"함수형 개발자는 변경 가능한 상태를 쓰지 않는다"라는 말은 틀렸다.(아마 많은 소프트웨어가 변경 가능한 상태를 남용하여 이런 말이 생긴 것 같다)

 

모든 소프트웨어는 변경 가능한 상태를 잘 관리해야 한다. 중요한 것은 상태를 가능한 한 안전하게 사용하는 것이다.

  • 셀은 변경할 수 있지만 변경 불가능한 변수에 값을 담아두기 때문에 전역 변수보다 더 안전하다.

`ValueCell`의 `update()` 메서드를 사용하면 현재 값을 항상 올바르게 유지할 수 있다.

  • `update()`를 사용할 때 계산을 넘기기 때문이다. 현재 값이 도메인 상에서 올바른 값이고 계산이 항상 올바른 값을 리턴한다면 `update()`는 항상 올바른 값으로 새로운 값을 리턴하기 때문에 항상 올바른 값을 유지한다.
  • 단, 다른 타임라인에서 읽거나 쓰는 순서를 보장하지 않는다.
`ValueCell`을 일관되게 유지하기 위한 안내
- 올바른 값으로 초기화하기
- `update()`에는 계산을 전달하기(절대 액션을 전달하지 말기)
- 계산은 올바른 값이 주어졌다면 올바른 값을 리턴하기

반응형 아키텍처를 사용하여 바꾼 시스템의 결과

영향 1 : 원인과 효과가 결합된 것을 분리한다

스펙

현재 장바구니에 있는 제품이 무료 배송 기준에 맞으면 무료 배송 아이콘을 표시해야 한다.

  • 현재 장바구니라는 조건은 잘 봐야 한다. 즉 장바구니 상태가 변경될 때마다 배송 아이콘을 갱신해야 한다는 뜻이다.
  • 장바구니가 바뀌는 경우는 많다 : 제품 추가/삭제, 장바구니 비우기 등
기존 개선
버튼 클릭이라는 원인과 그로 인해 발생되는 배송 아이콘 갱신이라는 효과가 결합되어 있다.
장바구니가 바뀌는 경우마다 배송 아이콘을 갱신하는 코드를 추가해야한다.
원인과 효과를 분리한다.
어떤 원인에 의해 장바구니가 변경되더라도 배송 아이콘을 갱신한다. 이때 배송 아이콘 갱신은 한 번만 만들면 된다.

 

결합의 분리는 원인과 효과의 중심을 관리한다

문제 : 장바구니를 바꾸는 방법과 장바구니가 바뀔 때 해야 할 일이 많은 것이 문제이다.

  • 만약에 장바구니가 바뀔 때 해야 할 일을 추가하기 위해 장바구니를 바꾸는 코드를 모두 고쳐야 한다. 장바구니를 바꾸는 방법을 하나 추가해도 마찬가지다. 즉 어느 쪽이 추가되어도 고쳐야 할 것이 많다.
  • 이는 원인과 결과가 모두 연결되어 있기 때문에 관리해야 할 개수는 "바꾸는 방법 개수(원인) X 할 일 개수(효과)"만큼으로 늘어난다. 여기서 전역 장바구니는 원인과 결과의 중심이라고 할 수 있고, 이를 잘 관리해야 한다.

장점 : 원인과 효과를 분리하여 해결한다. 관리할 것이 늘어나는 문제를 곱셈에서 덧셈으로 바꿀 수 있다.

  • 효과를 추가해도 원인을 고치지 않아도 되고, 원인을 추가해도 효과를 고치지 않아도 된다. 따라서 하나가 추가되면 하나만 늘어난다.
  • 따라서 이벤트 핸들러는 장바구니를 바꾸는 일에만 신경 쓰면 되고, DOM을 갱신하는 곳에서는 DOM 갱신만 신경 쓰면 된다.

주의 : 문제가 없는 데 이 방법으로 분리하는 것은 좋지 않다.

  • 코드에 액션을 순서대로 표현하는 것이 더 명확할 수 있다.

영향 2 : 여러 단계를 파이프라인으로 처리한다

반응형 아키텍처도 간단한 액션과 계산을 조합해 복잡한 동작을 만든다. 즉 파이프라인을 만든다.

  • 파이프라인 : 액션과 계산을 조합한 하나의 액션
  • 데이터가 파이프라인으로 들어가 각 단계에서 처리된다.

어떤 일이 발생하는 여러 단계가 있다면 파이프라인으로 처리하는 것이 좋다.

 

장점 : 이를 사용하면 함수가 간단해진다. 만들어진 간단한 함수는 재사용하기 좋다.

주의 : 여러 단계가 있지만 데이터를 전달하지 않는다면 이 패턴을 사용하지 않는 것이 좋다.

  • 데이터를 전달하지 않으면 파이프라인이라고 볼 수 없다. 따라서 올바른 반응형 아키텍처가 될 수 없다.
라이브러리
- ReactiveX : 단일 이벤트 대신 이벤트 스트림이 필요할 경우 사용
- Kafka, RabbitMQ : 외부 스트림 서비스(스트림 서비스를 사용하면 반응형 아키텍처를 더 큰 범위(액션, 계산 -> 서비스 관점)에서 적용할 수 있다)

영향 3 : 타임라인이 유연해진다.

순서를 정의하는 방법을 뒤집기 때문에 자연스럽게 타임라인이 작은 부분으로 분리된다.

15장에서 짧은 타임라인이 좋은 것이라고 했지만 타임라인이 많아지는 것도 좋지 않다. 하지만 공유하는 자원이 없으면 타임라인이 많아져도 문제가 없다.

`ValueCell`, `FormulaCell` : 감시자를 호출할 때 현재 값은 넘겨주기 때문에 직접 장바구니 값을 읽지 않아도 된다. 따라서 장바구니 값을 전역변수로 사용하지 않아도 된다.(= 공유하는 자원이 없다/안전하다)

어니언 아키텍처

서비스 전체를 구성하는 데 사용하기 때문에 바깥 세계와 상호작용 하는 부분을 다룬다.


특정 계층이 꼭 필요하다고 강제하진 않지만 많은 경우에 아래와 같은 3가지 큰 분류로 나눌 수 있다.

  • 인터랙션 계층
    • 바깥세상에 영향을 주거나 받는 액션
  • 도메인 계층
    • 비즈니스 규칙을 정의하는 계산
  • 언어 계층
    • 언어 유틸리티와 라이브러리

위의 간단한 그림으로 함수형 시스템이 잘 동작할 수 있는 중요한 규칙을 알 수 있다.

  1. 현실 세계와 상호작용은 인터랙션 계층에서 해야 한다.
  2. 계층에서 호출하는 방향은 중심 방향이다.
  3. 계층은 외부에 어떤 계층이 있는지 모른다.
어니언 아키턱체는 파트 Ⅰ에서 배운 액션과 계산의 분리, 계층형 설계 방식과 잘 맞다.

다시 보기 : 액션과 계산, 데이터
- 데이터 : 이벤트에 대한 사실 / 수동적이고 투명함 / 예) 숫자나 문자열 등
- 계산 : 입력으로 출력을 만드는 연산 / 같은 입력 같은 결과 / 호출 시점이나 횟수에 영향 X
- 액션 : 바깥 세계에 영향을 주거나 받는 실행 가능한 코드 / 호출 시점이나 횟수가 중요 / 예) 데이터베이스, API, 웹 요청 등

다시 보기 : 계층형 설계

- 함수 호출 관계를 기반으로 함수를 배치하는 방법
- 어떤 함수가 재사용하기 좋고 변경하기 쉬운지, 테스트할 가치가 높은 코드가 무엇인지 알 수 있다
  - 아래의 계층일수록 재사용하기 쉽고고, 자주 바뀌지 않으며, 테스트할 가치가 높다
- 어떤 박스가 액션이라면 박스 위쪽 경로에 있는 모든 박스는 액션이 된다

전통적인 계층형 아키텍처

전통적인 아키텍처로 웹 API를 만들 때 계층(layer)이라는 개념을 사용한다. 이는 어니언 아키텍처의 계층과는 다르다.

  • 웹 인터페이스 계층 : 웹 요청을 도메인으로 바꾸고 도메인을 웹 응답으로 바꾼다.
  • 도메인 계층 : 애플리케이션 핵심 로직으로 도메인 개념에 DB 쿼리나 명령이 들어간다.
  • 데이터베이스 계층 : 시간에 따라 바뀌는 정보를 저장한다.

전통적인 계층형 아키텍처는 데이터베이스를 기반으로 한다.

  • 도메인 계층은 데이터베이스 동작으로 만든다.
  • 웹 인터페이스는 웹 요청을 도메인 동작으로 변환한다.

데이터 베이스 계층이 가장 아래에 있다면 그 위에 있는 모든 것이 액션이 되기 때문에 함수형 스타일이 아니다.

함수형 아키텍처

함수형 아키텍처와 전통적인 아키텍처의 주요 차이점은 "데이터베이스와 계층과 도메인 계층의 관계"이다.

  • 함수형 아키텍처는 도메인 계층이 데이터베이스 계층에 의존하지 않는다.
    • 함수형 개발자는 액션과 계산을 명확하게 구분하려고 하고, 도메인 로직은 모두 계산으로 만들어야 한다
    • 가장 위에 있는 액션에서 도메인 규칙과 데이터베이스를 조합한다.

액션과 계산을 구분하는 선을 그리고 라이브러리나 언어 기능과 계산을 구분하는 선을 그려 각 아키텍처를 표현하면 다음과 같다.

위의 함수형 아키텍처 그림에서 각 점선 끝을 연결하면 어니언 아키텍처와 같은 모양이 된다.

변경과 재사용이 쉬워야 한다(어니언 아키텍처와 맞다)

어니언 아키텍처는 인터랙션 계층을 바꾸기 쉽다. 그래서 도메인 계층을 재사용하기 좋다.

  • 인터랙션 계층은 바꾸기 쉽다
    • 계층의 가장 위에 있기 때문에 가장 바꾸기 쉽다.
    • 도메인이 데이터베이스나 웹 요청에 의존하지 않아 인터랙션 계층에 속하는 데이터베이스나 서비스 프로토콜은 쉽게 바꿀 수 있다.
  • 도메인 계층은 재사용하기 좋다
    • 외부 서비스(데이터베이스나 서비스)에 의존하지 않는다. 따라서 전부 계산으로 만들 수 있다.

정리하면, 어니언 아키텍처는 데이터베이스나 API 호출과 같은 외부 서비스를 바꾸기 쉽다. 이는 가장 높은 계층(인터렉션 계층)에서 사용하기 때문이다. 도메인 계층은 외부 서비스에 의존하지 않는다. 어니언 아키텍처는 좋은 인프라보다 좋은 도메인을 강조한다.

 

(전통적인 아키텍처 vs 어니언 아키텍처) 예) 장바구니 아이디로 데이터베이스에서 장바구니 가져오기

도메인 규칙에 DB를 사용하지 않고 처리할 수 있다. 아래 예를 확인해 보자.

전통적인 아키텍처 <<도메인 계층>>
웹 요청은 핸들러가 처리한다.

- DB에 접속하고 클라이언트에게 응답하기 위해 가장 높은 웹 계층으로 결과를 리턴한다.

장바구니 합계를 계산하는 도메인 규칙은 DB에서 합계를 가져와 처리한다.

- 도메인은 DB에 접근하므로 계산이 아니다.
어니언 아키텍처 <<인터랙션 계층>>
웹 서버, 핸들러, DB를 모두 포함한다.
- 핸들러가 DB에서 장바구니를 가져와 도메인에 전달한다.

<<도메인 계층>>
`cart_total()`
- 제품 가격을 가지고 장바구니 합계를 만드는 계산이다.
- 장바구니 데이터가 어디서 온지는 알 필요 없다.

 

도메인 규칙이 액션이 될 수도 있다.

도메인을 계산으로 만드는 것은 항상 가능하며, 권장된다. 파트 I에서 배운 대로 액션에서 계산을 빼내면 된다.

어떤 경우는 도메인이 액션이 될 수도 있다. 아래의 내용을 생각해 보고 계산을 될지 액션이 될지 판단하면 된다.

 

도메인 규칙은 도메인 용어를 사용한다

프로그램의 핵심 로직을 도메인 규칙 도는 비즈니스 규칙이라고 한다. 모든 로직이 도메인 규칙이 아니므로 어떤 로직이 도메인 규칙인지 판단하기 위해 코드에 나타나는 용어를 참고할 수 있다.

  • 도메인 용어 : 제품, 이미지, 가격, 할인 등
  • 인터렉션 용어(도메인이 아닌 용어) : 새로운/이전 데이터베이스, 웹 요청 실패 시 재시도, ajax 요청

가독성을 따져야 한다

특정 패러다임의 장점이 항상 좋은 것은 아니다. 도메인을 항상 계산으로 만들 수 있지만, 어떤 경우는 문맥에 따라 계산보다 액션이 읽기 좋은 경우가 있다.

 

가독성을 결정하는 요소는 여러 가지다.

  • 사용하는 언어
  • 사용하는 라이브러리
  • 레거시 코드와 코드 스타일
  • 개발자들의 습관

어니언 아키텍처는 가장 이상적인 모습이다. 하지만 세상에 완벽한 것은 없으므로 현실 세계의 문제와 이상적인 다이어그램 사이를 균형 있게 유지해야 한다. 아래와 같은 항목들과 타협해야 한다.

 

코드의 가독성

  • 일반적으로 함수형 코드는 읽기 좋다. 도메인 계층을 계산으로 만들어 인터렉션 계층과 분리하면서 읽기 좋은 코드를 만드려고 노력해야 한다.
  • 하지만 함수형 코드가 아닌 코드가 더 명확한 경우에 어느 게 더 나은지 판단해야 한다.

개발 속도

  • 비즈니스 이유로 기능을 빨리 출시해야 하는 경우도 있다. 이런 경우 많은 것을 타협해야 하기 때문에 나중에 아키텍처에 맞춰 코드를 정리할 준비를 하는 것이 좋다.
  • 계산을 추출하고 함수형 도구로 코드를 연결하고 타임라인을 잘 관리해야 한다.

시스템 성능

  • 변경 가능한 데이터 구조는 불변 데이터 구조보다 빠르다.
  • 성능 개선과 도메인을 계산을 만드는 것은 따로 생각하는 것이 좋다.
    • 인터랙션 계층에서 최적화는 진행한다.
    • 도메인 계층에서 재사용 가능한 계산으로 만든다.

예) 작년 판매된 제품에 대한 보고서를 생성하는 함수에 할인 정보를 표시하는 기능을 추가하기

할인 레코드의 아이디는 제품 레코드에서 찾을 수 있는 데, 이는 optional 한 값이다.

 

먼저 작년 판매된 제품에 대한 보고서를 생성하는 함수는 다음과 같다.

이제 할인 정보를 표시하는 기능을 추가해 보자.

가장 쉬운 방법은 `reduce` 콜백에서 할인 레코드의 아이디로 DB에서 가져오는 것이다. 하지만 이렇게 하면 `generateReport()`가 액션이 된다. 도메인을 계산으로 유지하기 위해 제품을 DB에서 가져오는 가장 상위 액션(인터렉션 계층)에서 이 작업을 수행하도록 해야 한다.

 

좋은 타임라인의 원칙

15장에서는 1~3번째 원칙, 16장에서는 4번째 원칙을 알아봤다. 이 장에서는 5번째 원칙에 대해 알아본다.

 

5. 시간을 일급으로 다룬다.

액션의 순서와 타이밍을 맞추는 것은 어렵다. 타임라인을 관리하는 재사용 가능한 객체를 만들면 타이밍 문제를 쉽게 처리할 수 있다. 중요한 것은 시간에 대한 관점이다. 호출 순서와 반복은 직접 다룰 수 있다.

 

모든 언어는 시간에 대한 암묵적인 모델을 가지고 있다. 하지만 이 모델이 우리가 해결하는 문제와 맞지 않는 모델인 경우가 있다. 함수형 프로그래밍으로 문제에 맞는 새로운 시간 모델을 만들 수 있다.

 

예 : 제품 추가에 대해 속도 개선과 최적화를 진행했다. 이로 인해 버그가 생겼다.

<버그>

  • 제품을 하나만 추가해도 가끔 잘못된 합계가 표시된다 : 제품 가격은 누락되고 배송비만 표시된다.

<코드 변경 사항 확인>

  • 기존 동작 : `shipping_ajax()`는 `cost_ajax()` 콜백 안에서 호출
  • 변경된 동작 : `shipping_ajax()`호출 후 `cost_ajax()`가 바로 호출(거의 동시에 실행된다고 생각하면 된다.)

이제 타임라인을 그려서 어떤 일이 벌어지는지 살펴보자.

액션 확인하기

1. 먼저 코드 내 액션을 파악한다.

2. (주의) `total`은 지역 변수이지만 액션에 포함한다. : 변경된 코드에서는 `total`이 여러 타임라인에서 사용하게 되었으므로 액션으로 분류한다.

 

액션 그리기&다이어그램 단순화하기

액션 그리기

  • 크게 클릭 핸들러, 큐, ajax(`cost_ajax()`, `shipping_ajax()`) 콜백에서 실행되는 부분으로 나누어 타임라인을 그린다.

다이어그램 단순화하기 1

  1. 하나의 타임라인에 있는 모든 액션을 하나로 통합한다.
  2. 타임라인이 끝나는 곳에서 새로운 타임라인이 하나만 생긴다면 통합한다.
    • 실행 순서를 명확히 하기 위해 점선을 단계가 끝나는 지점으로 옮긴다.

다이어그램 단순화하기 2

  1. 타임라인이 끝나는 곳에서 새로운 타임라인이 하나만 생긴다면 두 타임라인을 합친다.

실행 가능한 순서 분석하기

현재 타임라인이 공유하는 자원은 `total` 변수뿐이다. 이제 두 콜백 타임라인의 실행 가능한 순서를 알아보고 문제가 있는지 보자.



자바스크립트 스레드 모델에서 동시에 실행하는 것은 불가능하다. 기대한 동작이다
모든 값(가격과 배송비)이 `total` 변수에 합산되고 DOM이 업데이트된다.
잘못된 동작이다.
버그 : `cost_ajax()` 응답을 받기 전에 DOM이 업데이트된다.

 

버그 발견 : 두 콜백이 기대하지 않는 순서로 실행될 수 있다는 것을 확인했다. 요청 순서는 올바르지만 `shippnig_ajax()` 콜백이 `cost_ajax()` 콜백보다 먼저 실행될 수 있다.

 

변경한 타임라인이 빠른 이유

코드를 고치기 전에 왜 잘못된 코드(최적화 코드)가 더 빠르게 실행되는지 확인해 보자

만약 `cost_ajax()` 응답이 3초, `shipping_ajax()` 응답이 4초가 걸리다고 한다면 최적화 전/후 코드에서 걸리는 시간은 다음과 같다.

  • 이전 코드 : 두 응답을 순서대로 기다린다 / 7초(3+4)
  • 이후 코드 : 두 응답을 병렬로 기다린다 / 4초(3, 4의 최댓값)

Q. 타임라인에서 `cost_ajax` 요청을 `shipping_ajax` 보다 먼저 보냈는데 `cost_ajax` 응답이 `shipping_ajax`보다 가끔 늦어지는 이유는 무엇인가요?
A. 요청에 대한 응답이 순서에 맞지 않게 오는 원인은 다양하다. (컴퓨터와 서버 사이에 네트워크 상황은 혼란스러워서 언제나 문제가 생길 수 있다) 그중 몇 가지 원인은 다음과 같다.
  1. `cost_ajax` 응답의
크기가 커서 다운로드하는 데 시간이 오래 걸린다.
  2. 배송 API 서버보다 `cost_ajax`를 처리하는 서버가 더 바쁘다.
  3. 이동 중인 차 안에서 핸드폰으로 `cost_ajax` 요청을 보냈지만, 기지국이 바뀌는 동안 지연이 생겼다. `shipping_ajax`는 같은 기지국을 사용했기 때문에 빨랐다.

 

모든 병렬 콜백 기다리기 : 컷

실패 없이 병렬로 응답을 기다려 실행 속도를 개선할 수 있는 방법이 있을까?

버그 수정과 기존 최적화를 최대한 살려서 개선을 하자. 목표는 동시에 도착하는 ajax 응답을 모두 기다렸다가 DOM을 업데이트하면 된다.

 

위 다이어그램에서 점선을 컷(cut)이라고 부른다.

  • 설명
    • 순서를 보장해 주는 역할을 한다.
    • 컷은 앞에서 본 점선과 다르게 여러 타임라인의 끝에 맞춰 그린다. 타임라인에 컷이 있다면 컷 위에 있는 단계는 컷 아래 단계가 실행되기 전에 모두 끝나야 한다.
  • 좋은 점
    • 타임라인의 앞부분과 뒷부분으로 나누어서 액션이 서로 섞이지 않고 따로 분석할 수 있다
    • 실행 가능한 순서를 줄여 애플리케이션이 복잡성을 줄여준다.

어떤 순서로 처리될지 알 수 없지만 두 응답이 모두 처리되고 나서 마지막으로 DOM을 업데이트한다.

 

타임라인을 나누기 위한 동시성 기본형

목표 : 타임라인이 서로 다른 순서로 끝나도 모든 타임라인이 끝나야 계속 진행하도록 하는 기본형

 

자바스크립트는 단일 스레드이기 때문에 간단하게 동시성 기본형을 구현할 수 있다.

  • 단일 스레드여서 비동기 작업들이 서로 영향을 주지 않는다(각 작업이 끝날 때까지 다른 작업에 영향을 주지 않는다). 이를 활용해 변경할 수 있는 값( (`num_finished`)을 안전하게 공유한다.
  • 멀티 스레드를 지원하는 언어에서는 타임라인을 조율하기 위해 락과 같은 기능을 사용해야 한다.
동시성 기본형 함수( `Cut()` ) 활용 예제

 

코드에 `Cut()` 적용하기

적용하기 위해 두 가지를 고민해야 한다.

  1. `Cut()`을 보관할 범위
    응답 콜백 끝에서 `done()`을 불러야 한다. 따라서 두 응답 콜백을 만드는 `calc_cart_total()` 함수 범위에`Cut()`을 만드는 것이 좋다.
  2. `Cut()`에 전달할 콜백
    `calc_cart_total()`에는 `total` 값 계산이 끝났을 때 부르는 콜백이 이미 있다. 따라서 이 콜백을 전달하면 된다. 실제로는 `update_total_dom()` 콜백이 전달될 것이다.

Q. `Promise`처럼 다른 것을 사용하는 게 더 좋지 않나요?
A. 맞다. 각 언어는 나름의 동시성 기본형을 가지고 있다. 자바스크립트에서는 `Promise.all()`로 대체할 수 있다.
여기서는 자바스크립트가 아닌 함수형 프로그래밍을 알려주는 것이 목적이므로 원리를 알려주는 것이다. 따라서 기본형이 없는 곳에서도 구현할 수 있어야 한다.

 

변경된 코드에 대한 순서 분석하기

점선을 기준으로 확인하면 된다.

  1. 첫 번째 점선 위와 두 번째 점선 아래는 타임라인이 하나밖에 없으므로 실행 가능한 순서는 하나다.
  2. 두 점선 사이는 두 타임라인을 가지므로 확인해야 한다.

2번만 실행 순서를 나열하여 확인해 보자.

단일 스레드 환경에서는 불가능하다. 제품 가격을 `total`에 더하고 나서 배송비를 더한다. 배송비를 `total`에 더하고 제품 가격을 더한다.

 

타임라인 복잡성 검토

`Cut()`을 사용하면서 타임라인이 더 복잡해졌다. 이렇게 복잡한 것이 모두 필요한 것인가?

 

복잡성 원인 검토 해결 방법 고민(복잡성을 제거해볼까)
비동기 웹 요청 아키텍처 때문에 생기는 복잡성
- 자바스크립트 앱에서는 비동기 웹 요청을 사용해야한다.
ajax 요청을 사용하지 않으면 해결할 수 있다.
이러면 링크와 입력 폼을 전송하고 작은 변경에도 페이지가 새로 고쳐진다. => 원하는 결과가 아니다.
결과를 합쳐야 하는 두 개의 API 응답   API 하나로 처리하게 수정하면 해결할 수 있다.
이러면 동시에 요청하지 않아도 되고 응답을 조합하지 않아도된다.
하지만 이는 복잡성을 없앤 것이 아니다. 복잡성을 서버로 옮긴 것이다.
예측 불가능한 사용자 액션 아키텍처 때문에 생기는 복잡성
- 장바구니는 사용자가 조작할 수 있어야 하므로 인터렉션이 필요하다.
앱에서 사용자 인터렉션을 적게 만들면 해결할 수 있다.
사용자에게 입력 폼을 주고 구매하고 싶은 모든 것을 적고 전송하도록 고칠 수 있다. => 이는 UX가 좋지 않다.

 

복잡성은 필연적이지 않다. 복잡성을 바꾸지 않으려고 하는 선택들로 생긴다. 즉 선택한 것이 항상 고려되어야 한다면 복잡성은 피할 수 없다. 따라서 우리는 복잡성을 다룰 수 있는 좋은 프로그래밍 기술이 필요하다,

 

새로운 기능 요청 : 사용자가 제품을 장바구니에 처음 추가할 때만 메시지를 보내자

여러 번 호출해도 한 번만 실행되도록 만들 수 있는 동시성 기본형을 만들자.

// 메시지 보내기 함수
function sendAddToCartText(number) {
  sendTextAjax(number, "Thanks for adding somthing to your cart.");
}

// 최초 한 번만 액션을 실행하는 동시성 기본형 함수
function JustOnce(action) {		// 액션 전달
  var alreadyCalled = false;	// 함수가 실행됐는지 기억
  return function(a, b, c) {
    if(alreadyCalled) return;	// 실행한 적이 있다면 바로 종료
    alreadyCalled = true;		// 실행되었다는 사실을 기록
    return action(a, b, c);		// 인자와 함께 액션을 호출
  };
}

// 사용 사례
var sendAddToCartTextOnce = JustOnce(sendAddToCartText);

sendAddToCartTextOnce("555-5555");
sendAddToCartTextOnce("555-5555");
sendAddToCartTextOnce("555-5555");
sendAddToCartTextOnce("555-5555");

 

시간 모델 : 암묵적 시간 모델 / 명시적 시간 모델

자바스크립트는 다음과 같은 암묵적인  시간 모델을 가지고 있다.

  1. 순차적 구문은 순서대로 실행된다.
  2. 두 타임라인에 있는 단계는 왼쪽 먼저 실행되거나, 오른쪽 먼저 실행될 수 있다.
  3. 비동기 이벤트는 새로운 타임라인에서 실행된다.
  4. 액션은 호출할 때마다 실행된다.

장점

  • 간단하다.

단점

  • 실행 방식을 바꾸지 못한다.
    • 앱에서 필요한 실행 방식과 딱 맞을 일은 거의 없다. 따라서 함수형 개발자는 필요한 실행 방식에 가깝게 새로운 시간 모델을 만들 수 있어야 한다.(=명시적 시간 모델) 예) `JustOnce()`, 비동기 콜백을 사용할 때 새로운 타임라인을 만들지 않도록 하기 위해 큐 사용

 

좋은 타임라인의 원칙

15장에서는 1~3번째 원칙을 알아봤다. 이 장에서는 4번째 원칙에 대해 알아본다.

 

4. 자원을 공유한다면 서로 조율해야 한다.

공유 자원을 많이 없애도 여전히 없앨 수 없는 공유 자원이 남는다.

타임 라인은 공유 자원을 안전하게 공유할 수 있어야 한다.(= 올바르게 자원을 쓰고 돌려준다.)

타임 라인을 조율한다는 것은 실행 가능한 순서를 줄인다는 것을 의미한다. 그 과정에서 올바른 결과가 나오지 않는 순서를 없애면 분석하기 쉬워진다.

예 : 제품 구매 버튼을 빠르게 여러 번 클릭했을 때 장바구니에 합계 금액이 잘 못 표시되는 문제

15장에 이어서 나오는 예제이며, 여기서는 '공유 자원'문제를 해결한다.

DOM 자원을 공유함으로써 문제가 생긴다

버그는 DOM 자원을 공유하기 때문에 생긴다.

 

두 액션이 자원을 공유하지 않는다면 실행 순서를 신경 쓰지 않아도 된다. 하지만 자원 공유시에는 실행 순서가 중요하다.

  • 두 타임라인은 DOM을 공유하고 있기 때문에 잠재적으로 문제가 있다.

 

다음은 DOM을 업데이트하는 두 액션이 실행할 때 가능한 순서이다.

 

<왼쪽 먼저 : 기대한 결과>

두 번째 DOM 업데이트가 최신 정보이기 때문에 두 번째 DOM 업데이트로 첫 번째 DOM이 업데이트 한 값을 덮어써야 한다.

 

<오른쪽 먼저 : 기대하지 않은 결과>

두 번째 합계를 첫 번째 합계가 덮어쓰면 안 된다. 그러면 두 번째 누른 아이템 값이 누락된다.

 

 

문제 해결을 위해 DOM이 업데이트되는 순서를 보장해야 한다 : 큐

업데이트 순서를 항상 왼쪽이 먼저 실행되도록 제한해야 한다.(= 클릭한 순서 DOM이 업데이트돼야 한다.)

하지만 DOM 업데이트는 아무도 통제할 수 없다.

  • 두 클릭은 서로 클릭했는지 모르며, DOM 업데이트는 네트워크 요청이 완료되는 시점에 실행되기 때문이다.

올바른 순서로 업데이트되도록 DOM 업데이트를 조율할 방법이 필요하다 : 를 활용해 보자. 

  • 큐는 여러 타임 라인에 있는 액션 순서를 조율하기 위해 많이 사용한다.
  • 큐는 모든 작업을 같은 타임라인에서 처리함으로써 순서대로 작업을 꺼내쓸 수 있기 때문에 공유 자원을 안전하게(=순서대로) 공유할 수 있다.
타임라인이 다음 자원을 공유하면 문제가 될 수 있다:
- 전역 변수, DOM, 지역변수를 공유하는 것, 데이터 베이스, API 호출

 

자바스크립트에서 큐를 만들어, 타임라인 조율하기

자바스크립트에는 큐 자료 구조가 없기 때문에 만들어야 한다.

큐를 타임라인 조율에 사용한다면 이를 동시성 기본형(concurrency primitive)라고 부른다.

  • 동시성 기본형 : 자원을 안전하게 공유할 수 있는 재사용 가능한 코드

먼저 큐에서 할 일과 클릭 핸들러에서 할 일을 나누어보자.

  • 큐에서 할 일
    • `cost_ajax()` 부터의 작업
  • 클릭 핸들러에서 할 일(다른 작업이 끼어들 수 없어 순서가 섞일 염려가 없다)
    • `cost_ajax()` 전 작업 : `cost_ajax()`가 첫 비동기 호출이기 때문에 다음에 순서가 끼어들 수 있는 첫 번째 액션이다.

아래의 순서로 코드를 수정해 보자.

  1. 큐에서 처리할 작업을 큐에 넣기(주석 1, 2)
    • 큐에서 처리할 작업을 큐에 넣는 액션 하나로 바꾼다.
  2. 큐에 있는 첫 번째 항목을 실행한다(주석 3, 4)
  3. 두 번째 타임라인이 첫 번째 타임라인과 동시에 실행되는 것을 막기(주석 5, 6)
    • 이미 실행되는 작업이 있는지 확인해서 두 타임라인이 섞이지 않게 한다.
    • `working` 전역 변수를 추가해 관리한다.
  4. 다음 작업을 시작할 수 있도록 `calc_cart_total()` 콜백 함수 고치기(주석 7, 8)
    • 장바구니에 추가된 작업이 항상 하나만 실행된다. 현재 작업이 끝날을 때 다음 작업을 실행할 수 있도록 고쳐보자.
    • 작업 완료를 표시하고 다음 작업을 실행한다.
  5. 항목이 없을 때 멈추게 하기(주석 9)
  6. 변수와 함수를 함수 범위로 넣기(주석 10)
    • `Queue()`라는 함수에 전역 변수와 사용하는 함수를 넣어 다른 곳에서 접근할 수 없도록 한다.
/* 새로운 코드 */
// 10. 함수에 큐를 관리하는 변수를 정의해서 다른 곳에서 접근할 수 없도록 한다.
function Queue() {
  var queue_item = [];
  var working = false; // 현재 동작하고 있는 다른 작업이 있는지 확인
  
  function runNext() {
    // 6. 동시에 두 개가 동작하는 것을 막는다.
    if(working)
      return;
    // 9. 작업할 항목이 없으면 멈춘다.
    if(queue_items.length === 0)
      return;
    // 5. 동작하는 작업 존재 표시
    working = true;
    // 4. 배열에 첫 번째 항목을 꺼내서 cart에 넣는다.
    var cart = queue_items.shift();
    calc_cart_total(cart, function(total) {
      update_total_dom(total);
      // 7. 작업 완료 표시
      working = false;
      // 8. 다음 작업 시작 : 비동기로 작업을 이어서 할 수 있는 중요한 반복 구조를 만듬!
      runNext();
    });
  }
  
  return function(cart) {
    // 2. 큐에 항목을 추가한다.
    queue_item.push(cart);
    // 3. 큐 워커를 실행한다. setTimeout()은 자바스크립트 이벤트 루프에 작업을 추가한다.
    setTimeout(runNext, 0);
  };
}

var update_total_queue = Queue();

/* 기존 코드 기반 개선 : 변경된 부분엔 주석 추가 */
function add_item_to_cart(item) {
  cart = add_item(cart, item);
  // 1. 클릭 핸들러에서 큐에 항목을 추가한다.
  //    기존에 호출했던 `calc_cart_total()`는 큐에서 처리하도록 변경한다.
  update_total_queue(cart);
}

function calc_cart_total(cart, callback) {
  var total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
    shipping_ajax(cart, function(shipping) {
      total += shipping;
      callback(total);
    });
  });
}

 

[다이어그램]

Q. 왜 콜백 안에서 `runNext()`를 호출해야 하나요? `calc_cart_total()` 다음에 `runNext()`를 호출하면 안 되나요?
A. `calc_cart_total()`이 비동기 호출이기 때문이다. 이 함수 안에서 두 ajax 호출에 대한 응답이 이벤트 큐에 추가되고 나중에 이벤트 루프에서 처리된다. 따라서 비동기 호출이 완료된 후 `runNext()`를 호출해야 한다.

 

원칙 : 공유하는 방법을 현실에서 착안하기

인간은 언제나 자원을 공유한다. 하지만 컴퓨터는 공유하는 방법을 모르기 때문에 직접 프로그래밍을 해줘야 공유가 가능하다.

  • 큐 : 사람들이 자원을 공유하기 위해 줄을 서는 것을 보고 만들어졌다. ex) 화장실, 은행, 푸드트럭,...
  • 줄 서는 것은 일반적이지만 모든 경우에 좋은 것은 아니다.
    • 기다려야 한다는 단점이 있다.
    • 기다리지 않고 자원을 공유하는 방법도 있다.
      • 한 번에 한 명씩 쓸 수 있게 화장실 문을 잠글 수 있다.
      • 공공 도서관(책이 모인 곳)은 지역사회가 많은 책을 공유할 수 있는 곳이다.
      • 칠판을 사용하면 선생님(기록하는 사람) 한 명이 교실 전체(읽는 사람)에 정보를 공유할 수 있다.

큐를 재사용할 수 있도록 만들기

AS-IS : 장바구니에 제품을 추가하는 기능에 특화됨

TO-BE : 일반적인 큐 + 작업이 끝났을 때 실행하는 콜백 받기

 

함수 본문을 콜백으로 바꾸기 리팩터링으로 큐를 반복해서 처리하는 코드(`runNext()`)와 큐에서 하는 일(`calc_cart_total()`)을 분리할 수 있다.

 

1. 일반적인 큐 만들기

// 3.구체적인 작업 내용이 있는 함수를 새로운 인자로 추가
function Queue(worker) {
  var queue_item = [];
  var working = false;
  
  function runNext() {
    if(working)
      return;
    if(queue_items.length === 0)
      return;
    working = true;
    var cart = queue_items.shift();
    
    worker(cart, function() {
      // 1. Queue의 일반적인 동작 분리
      working = false;
      runNext();
    });
  }
  
  return function(cart) {
    queue_item.push(cart);
    setTimeout(runNext, 0);
  };
}

// 2. 큐 작업(done 콜백으로 처리) 외 구체적인 작업을 실행하는 함수를 분리
// - cart : 인자로 받아 지역적으로 사용
// - done : 콜백 함수로 받아 큐의 작업 완료 처리 및 다음 동작 실행하는 동작(1번)을 호출한다. 
function calc_cart_worker(cart, done) {
  calc_cart_total(cart, function(total) {
    update_total_dom(total);
    done(total);
  });
}

// 4. 큐 정의시 전달
var update_total_queue = Queue(calc_cart_worker);

// 아래 코드는 기존과 동일
function add_item_to_cart(item) {
  cart = add_item(cart, item);
  update_total_queue(cart);
}

function calc_cart_total(cart, callback) {
  var total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
    shipping_ajax(cart, function(shipping) {
      total += shipping;
      callback(total);
    });
  });
}

 

2. (추가 요구 사항) 작업이 끝났을 때 실행하는 콜백 받기 : `item`에 데이터와 완료될 때 부르는 콜백을 같이 전달한다.

function Queue(worker) {
  var queue_item = [];
  var working = false;
  
  function runNext() {
    if(working)
      return;
    if(queue_items.length === 0)
      return;
    working = true;
    // 0. 일반적인 변수명으로 변경
    var item = queue_items.shift();
    // 2. item.data : worker에는 데이터만 전달
    // 3. 콜백에 val을 전달
    worker(item.data, function(val) {
      working = false;
      // 4. item.callback을 비동기로 호출, val를 전달
      setTimeout(item.callback, 0, val);
      runNext();
    });
  }
  
  // 1. 배열에 데이터와 콜백을 모두 넣는다.
  return function(data, callback) {
    queue_item.push({
      data: data,
      callback: callback || function() {}
    });
    setTimeout(runNext, 0);
  };
}

// 아래 코드는 기존과 동일
function calc_cart_worker(cart, done) {
  calc_cart_total(cart, function(total) {
    update_total_dom(total);
    done(total);
  });
}

var update_total_queue = Queue(calc_cart_worker);

function add_item_to_cart(item) {
  cart = add_item(cart, item);
  // 5. 작업 완료시 콜백은 2번째 인자로 전달 가능!!
  update_total_queue(cart);
}

function calc_cart_total(cart, callback) {
  var total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
    shipping_ajax(cart, function(shipping) {
      total += shipping;
      callback(total);
    });
  });
}

 

 

[정리]

`Queue()`는 액션에 순서 보장(guranteeing order)을 줄 수 있는 고차 함수(함수를 인자로 받아 함수를 리턴)이다.

  • 어떤 함수를 새로운 타임라인에서 실행하고 한 번에 한 타임라인만 실행할 수 있도록 만들어준다.
  • `Queue()`라는 말 대신 `linearize()`라고 할 수 있다.
  • `Queue()`는 동시성 기본형(concurrency primitive)이다. 
순서 보장이 안되는 타임라인
: 다른 타임라인에서 `run()`을 부르면 순서가 섞일 수도 있고 서로 영향을 준다.
순서 보장이 되는 타임라인
: 한 번에 하나씩 순서대로 `run()`을 부른다.

 

지금까지 만든 타임라인 분석하기

공유하는 리소스는 아이콘을 사용하여 강조한다.

장바구니 전역 변수는 먼저 클릭한 타임라인에서 항상 먼저 실행된다.(순서 문제가 생기지 않는다.)

  • 내부를 보면 한번 클릭 시 3번 접근하는데, 모두 같은 박스에서 동기적으로 실행된다.
  • 점선 때문에 의도와 다르게 실행되는 것은 불가능하다.
    • 점선은 이벤트 큐가 실행 순서를 보장한다는 것을 나타낸다.

DOM도 순서 문제가 생기지 않는다.

  • 큐를 사용해 DOM 업데이트를 같은 타임라인에서 하도록 만들었기 때문에 항상 순서대로 실행된다.

를 순서가 바뀔 수 있다! 하지만 가능한 두 순서 모두 올바른 결과가 나온다.

  • 큐는 모든 타임라인의 서로 다른 단계에서 공유하고 있다.
  • 2번째로 '큐에 추가'동작과 1번째로 '큐에서 꺼냄'동작의 실행 순서가 섞일 수 있다. 하지만 가능한 두 실행 순서는 모두 올바른 동작이다.

 

원칙: 문제가 있을 것 같으면 타임라인 다이어그램을 살펴보세요

타이밍 문제를 명확히 보여준다. 공유된 자원이 잘못된 순서로 사용되는지 알 수 있다. 그래서 다이어그램을 그리는 것이 좋다.

타이밍에 관한 버그는 재현하기 매우 힘들다.(이를 보완한다)

만약 액션을 사용하는 프로그래밍을 한다면 타임라인 다이어그램을 그려라. 이는 모든 실행 가능한 순서를 포함해 소프트웨어가 어떻게 동작하는지 이해하는 데 쓸 수 있는 유연한 도구다.

 

추가 요구사항 : 큐를 건너뛰도록 만들기

지금까지 구현한 코드는 항상 올바른 순서로 동작한다. 하지만 너무 느리다는 단점이 있다.

  • 워커는 순차적으로 작업을 실행하기 때문이다.

만약 누군가가 정말 빠르게 장바구니에 제품을 담는 클릭을 4번 한다고 생각해 보자. 사용자 입자에서는 마지막 합계만 DOM에 표시되면 되지만, 현재 구현으로는 클릭당 순차적으로 DOM이 업데이트된다.

 

마지막 업데이트만 하도록 코드를 수정해 보자. 새로운 작업이 들어오면 건너뛸 수 있도록 드로핑(dropping) 큐를 만들어보자.

  • 드로핑 큐를 적용한 `update_total_queue`는 아무리 빨리 항목을 추가해도 큐 항목이 한 개 이상 늘어나지 않는다.
  • 사용자는 계속 기다릴 필요 없이 서버에 응답을 최대 두 번만 기다리면 된다.

+ Recent posts