20161105

React 15 Stateless Component 패턴

React 15 정착하면서 팀 룰로 정한 stateless functional component 패턴.

component 책임에 따라 두 종류로 나눔.

Stateless (Presentational) — props 받아서 JSX 내놓는 것만 하는 컴포넌트. 이벤트는 callback prop으로 위임. state 없음. 대부분은 이 형태로 충분.

const Badge = ({ text, onClick }) => (
  <span className="badge" onClick={onClick}>{text}</span>
);

Badge.propTypes = {
  text: PropTypes.string.isRequired,
  onClick: PropTypes.func
};

Container (Class) — state 관리, lifecycle 필요한 경우. 데이터 fetch, Redux 연결 등.

class UserList extends React.Component {
  state = { users: [], loading: true };
  componentDidMount() {
    fetch('/api/users')
      .then(r => r.json())
      .then(users => this.setState({ users, loading: false }));
  }
  render() {
    if (this.state.loading) return <Spinner/>;
    return (
      <ul>{this.state.users.map(u => <Badge key={u.id} text={u.name}/>)}</ul>
    );
  }
}

효과:

  • 컴포넌트 대부분이 stateless라 테스트가 엄청 쉬움. 그냥 props 넘기고 스냅샷 확인
  • 재사용성 높아짐 — 로직 없이 표현만 담당하니까 다른 화면에도 붙일 수 있음
  • 공식 문서에서도 "가능하면 functional로 써라" 라고 권장

주의: stateless component에는 아직 shouldComponentUpdate 같은 lifecycle 못 달아서 강제 memoize 필요하면 class로 돌아가야 함. React.PureComponent가 15부터 도입됐는데 이건 얕은 비교 기반이라 props에 객체가 깊게 있으면 안 먹음.

팀 컨벤션: 먼저 stateless로 작성 → 상태/사이드이펙트 필요해지면 class로 승격. 역방향 (class 먼저 작성 후 functional로 분해)은 하지 않도록. 이 습관이 리팩토링 시간 꽤 줄여줌.

20161019

PostgreSQL 9.6 병렬 쿼리 체감

PostgreSQL 9.6 올리고 가장 기대했던 병렬 쿼리 체감 테스트.

세팅. 기본값은 max_parallel_workers_per_gather = 2. 늘려봐야 의미 있는 쿼리에서만 효과 봄.

max_worker_processes = 8
max_parallel_workers_per_gather = 4

테스트 쿼리: 1억 행 테이블에서 aggregate. 컬럼 인덱스 없음 (full scan 상황).

SELECT country, COUNT(*) FROM sales_log GROUP BY country;

-- 9.5 (직렬): 38.2 s
-- 9.6 (4 worker): 11.6 s

약 3.3배. 워커 수만큼 그대로 스케일 안 되는건 마스터 프로세스에서 집계/합치는 비용 때문. 그래도 체감은 확실.

병렬이 실제 적용되는지는 EXPLAIN ANALYZE에서 Gather 노드가 보이는지로 확인. 없으면 옵티마이저가 병렬을 안 택한거.

제약 많음:

  • CTE는 기본적으로 병렬 안됨
  • Trigger 있는 테이블은 제약
  • 사용자 정의 함수 대부분 PARALLEL UNSAFE로 간주되어 병렬 안됨. CREATE FUNCTION ... PARALLEL SAFE 명시해야 함
  • 작은 테이블엔 병렬이 오히려 오버헤드. min_parallel_relation_size 파라미터로 임계값 조정 가능

OLAP 성격의 쿼리 (집계, 리포트)에는 진짜 효과 있음. OLTP (짧은 쿼리 많은 경우)엔 영향 없음.

추가로 9.6에서 눈에 띄는 것: freeze 튜닝 자동화, replication slot 사용성 개선. upsert는 이미 9.5에 들어왔고 이번엔 내부 최적화 위주라고 보면 됨. 우리 DWH 목적 DB에는 다음 달 중 9.6 반영 예정.

20160916

Go 1.7 context 패키지로 정리된 코드

Go 1.7에서 context 패키지가 드디어 표준 라이브러리로 올라왔다. 기존 golang.org/x/net/context 쓰던 코드 갈아치우는중. 이제 net/http, database/sql도 context 받는 함수 시그니처가 표준으로 생김.

package main

import (
    "context"
    "database/sql"
    "net/http"
    "time"
)

func handler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    var name string
    err := db.QueryRowContext(ctx,
        "SELECT name FROM user WHERE id = ?", 42).Scan(&name)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    w.Write([]byte(name))
}

HTTP 요청 컨텍스트에 2초 타임아웃 걸고 DB 쿼리로 내려보냄. 핸들러가 타임아웃되면 DB 쿼리도 자동 취소됨 (드라이버가 지원하는 경우). 이 전파가 자동이라 너무 편함.

패턴:

  1. handler 최상단에서 r.Context()로 시작
  2. 필요시 WithTimeout, WithCancel로 감쌈
  3. 다운스트림 (DB, 외부 API) 호출에 계속 전달
  4. goroutine 띄우면 반드시 ctx.Done() 체크 루프

context.Value에 요청 ID나 로깅용 메타 넣는것도 유용. 단 context.Value에 너무 많이 의존하면 안티패턴. 진짜 "요청 전체에 흐르는 값" 만 담아야 함. 비즈니스 로직 파라미터를 context에 숨기는건 나쁨.

기존 코드 마이그레이션은 간단함:

// before
import "golang.org/x/net/context"
// after
import "context"

x/net/context와 표준 context는 type 호환됨 (전자는 후자의 type alias 수준). 기존 서드파티 라이브러리가 x/net/context 받아도 표준 context 넘기면 그대로 됨.

Go 1.5때 GC 스톱타임 줄인 것도 체감 컸는데, 1.7은 context 표준화가 가장 크다. 이거 없이 프로덕션 서버 못 쓰겠다.

20160822

Kubernetes 1.3 실사용 감상

Kubernetes 1.3 올라왔고 회사에서 파일럿 클러스터 구축 지시. 몇 주 써본 소감.

환경: 자체 서버 3노드 (kubeadm 아직 공식 아님 → 1.4 쯤 추가 예정이라는 얘기. 우리는 kube-up.sh 대신 수동 설치). ubuntu 14.04 위에 etcd, kube-apiserver 등 수동 배치. 설치가 정말 빡셈.

처음 부딪힌 개념들:

  • Pod — 컨테이너의 단위라기보다 "같은 lifecycle을 공유하는 컨테이너 그룹"
  • ReplicationController → 1.2부터 ReplicaSet + Deployment로 바뀜. 이제 Deployment 쓰면 됨
  • Service — Pod의 IP가 바뀌어도 안정적 접근점 제공. ClusterIP / NodePort / LoadBalancer
  • ConfigMap / Secret — 1.2부터 공식 stable. 환경변수/설정파일 주입

간단한 deploy + service yaml:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  template:
    metadata:
      labels: { app: web }
    spec:
      containers:
      - name: web
        image: registry.internal/web:1.2.3
        ports: [{ containerPort: 8080 }]
---
apiVersion: v1
kind: Service
metadata: { name: web }
spec:
  selector: { app: web }
  ports: [{ port: 80, targetPort: 8080 }]
  type: ClusterIP

kubectl apply -f deploy.yaml 로 반영.

좋은 점: 롤링 업데이트가 진짜 쉬움. 이미지 태그만 바꿔 kubectl set image 하면 자동으로 순차 교체. health check 기반이라 새 pod 준비된 다음에 예전 pod 삭제.

빡센 점:

  • 네트워킹. 오버레이 네트워크 필수 (우리는 flannel). CNI 세팅 안 맞으면 Pod끼리 통신 안 됨
  • 영속 볼륨. PV/PVC 개념이 복잡. NFS로 대충 붙여놨는데 production은 더 고민해야 함
  • Ingress는 1.3 기준 아직 베타. nginx ingress controller 따로 띄워서 검토중
  • 모니터링/로그. 기본 툴 부족. Heapster + Grafana 세팅 권장

학습 곡선이 진짜 가파른데, 갖춰놓고 나면 배포는 너무 편함. 3개월쯤 운영해보고 본서비스 이관 여부 판단 예정. 지금은 Docker Compose + swarm 조합이랑 병행.

20160720

TypeScript 2.0 strictNullChecks 도입기

TypeScript 2.0 RC 기준 strictNullChecks 켜고 기존 프로젝트 뜯어 고치는 중. 기대 이상으로 많이 잡힌다.

플래그 하나 켜면 nullundefined가 별도 타입이 되고, 모든 레퍼런스 타입에서 자동으로 nullable하지 않게 됨. 이걸 원할때만 T | null 명시.

// strictNullChecks OFF
function getUser(id: number): User {
  return users.find(u => u.id === id);
}
// ↑ find()는 undefined 반환할 수 있는데 타입체커가 봐주고 있었음

// strictNullChecks ON
function getUser(id: number): User | undefined {
  return users.find(u => u.id === id);
}
// 호출부에서도 undefined 처리 강제

기존 JS에서 은근 많이 쓰던 패턴이 다 경고로 뜸.

  • obj.foo.bar — obj가 null이면 런타임 에러. 타입체커가 이제 잡음
  • API 응답 매핑 후 optional 필드 접근 — 인터페이스에 ? 안 붙으면 non-null로 간주되던게 바뀜
  • arr.find(...) 반환값을 바로 프로퍼티 접근

Non-null assertion 연산자 ! 도 이번에 도입. "타입스크립트야 나는 확신한다" 라고 말해주는 용도.

const el = document.getElementById('root')!;  // HTMLElement (not HTMLElement | null)

남발하면 strict의 의미가 없어지지만 DOM API처럼 런타임에 확실히 있는걸 아는 경우엔 유용함.

우리 프로젝트는 파일 200개쯤 되는데 플래그 켜니까 경고 400개 나옴. 지금 절반 잡았음. 남는건 optional prop 매핑 쪽 싱클 대응이 대부분. 이거 하고 나면 런타임 "Cannot read property 'x' of undefined" 같은 에러가 확 줄어들 거 같다.

2.0 정식은 9월 예정. 꼭 써봐야 할 플래그.

20160519

Python 3.5 async/await 맛보기

Python 3.5의 async/await 문법 구경중. 그동안 3.4의 @asyncio.coroutine + yield from 로 했던거랑 비교해서 훨씬 문법적으로 깔끔해졌다.

기본 예:

import asyncio
import aiohttp

async def fetch(session, url):
    async with session.get(url) as resp:
        return await resp.text()

async def main():
    urls = ['http://a.example.com', 'http://b.example.com', 'http://c.example.com']
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, u) for u in urls]
        results = await asyncio.gather(*tasks)
        for r in results:
            print(len(r))

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

외부 API 여러 곳에 병렬로 요청 보낼 때 이만한게 없음. gather로 묶으면 3개가 대충 동시에 나감.

그런데 진짜 "async 컨텍스트로 안 들어갈 수 있으면 안 들어가는게 낫다" 는 감상도 같이 듦. 이유:

  • 블로킹 I/O (일반 requests.get 등) 한 번이라도 섞이면 이벤트 루프가 막힘. 생태계 전체가 async-ready여야 의미 있음
  • DB 드라이버도 aiomysql, asyncpg 같은 async 전용 드라이버 써야함. SQLAlchemy ORM은 아직 async 지원 약함
  • 디버깅이 동기 코드보다 훨씬 어려움. 스택트레이스가 불친절
  • threading으로도 충분한 케이스 많음 (I/O 적당히 분산되는 경우)

그래서 지금은 크롤링/외부 API 집계처럼 순수 I/O bound에 async를 쓰고, 일반 웹서비스는 여전히 WSGI + threading 기반. 나중에 sanic 같은 async 웹프레임워크가 무르익으면 그때 다시 검토.

3.5 자체는 LTS는 아닌데 2020년까지 유지보수 예정이라 당분간 3.5 기준으로 써도 될듯. 회사 프로젝트 하나 신규 건을 3.5로 시작해볼까 고민중.

20160407

Redis 3 클러스터 운영 메모

Redis 3.0 cluster 모드 운영 시작한지 석달. 장단점 메모.

우리 구성: 6노드 (마스터 3 + 슬레이브 3). 3노드씩 다른 서버에 분산. 해시슬롯 16384개를 마스터 3개가 나눠 가짐.

셋업

redis-trib.rb create --replicas 1 \
  10.0.1.11:6379 10.0.1.12:6379 10.0.1.13:6379 \
  10.0.1.11:6380 10.0.1.12:6380 10.0.1.13:6380

예전엔 redis-trib.rb (ruby) 로만 가능했는데 곧 redis-cli에 cluster 명령 통합될거라고 함.

클라이언트. redis-py의 경우 redis-py-cluster 별도 라이브러리 써야 함. MOVED 응답 받으면 클라이언트가 다른 노드로 재전송. 일반 redis-py로는 cluster 못 씀.

제약사항:

  • multi-key 연산은 모든 키가 같은 슬롯에 있을 때만 동작. MSET key1 v1 key2 v2 에서 key1/key2가 다른 마스터면 에러
  • hash tag로 같은 슬롯에 묶을 수 있음: user:{123}:profileuser:{123}:settings 는 {123} 기반으로 같은 슬롯
  • Lua 스크립트도 같은 슬롯 키만 가능
  • SELECT (DB 선택) 안됨. 0번 DB만 씀

장애 시나리오 경험담: 마스터 노드 하나가 메모리 부족으로 죽음. Redis sentinel 기반이 아니라 cluster 모드는 노드들끼리 gossip protocol로 감시. cluster-node-timeout (우리 설정 15초) 지나면 슬레이브가 마스터로 승격. 대충 20초 정도의 다운타임.

클라이언트 앱에서 이 기간 동안 에러를 어떻게 처리할지가 포인트. 우리는 재시도 로직을 래퍼에 넣어놔서 사용자 체감은 거의 없었음. 다만 재시도는 "읽기" 에만 적용. 쓰기는 retry하면 중복될 수 있으니 조심.

아쉬운 점: 샤드 리밸런싱이 아직 손이 많이 감. 자동 rebalance가 기본 활성화 아님. redis-trib.rb reshard 수동 실행. 클러스터 노드 추가/제거도 운영 절차 나름 복잡. AWS ElastiCache 쓸까 이런 고민도 함.

20160308

Vue 2 첫 프로젝트 회고

지난주 완료한 내부 툴 프론트엔드. Vue 2 (아직 RC) 써서 만듦. 첫 프로젝트 회고.

회사는 React 위주였는데, 이 프로젝트는 소규모 admin 툴이라 러닝커브 낮은 Vue 써봄. 진입 난이도가 놀랄만큼 낮았다. template / script / style 세 블록 구조가 직관적임.

<template>
  <div>
    <input v-model="q" @keyup.enter="search"/>
    <ul>
      <li v-for="item in results" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() { return { q: '', results: [] }; },
  methods: {
    search() {
      fetch('/api/items?q=' + this.q)
        .then(r => r.json())
        .then(data => { this.results = data; });
    }
  }
};
</script>

좋았던 점:

  • single file component (.vue) 가 너무 깔끔. JSX보다 읽기 편함
  • v-model로 양방향 바인딩 — 폼 처리가 진짜 짧아짐
  • Vuex 도입하면 Redux보다 boilerplate 적음. mutations / actions 구조가 단순
  • vue-cli 기본 webpack 템플릿이 잘 세팅되어 있음

아쉬운 점:

  • TypeScript 지원 아직 약함. 타입 안 쓰는 환경이면 문제 없지만 앞으로 갈지 고민
  • 커뮤니티 크기 React 대비 작음. 특정 문제 검색할때 영어 자료가 많지 않음 (그치만 중국어는 엄청 많다)
  • Vue 2 RC라 라이브러리들이 1.x 위주인 경우 있음. vue-router, vuex는 이미 2.x 대응됨

admin처럼 러닝커브 낮아야 하는 팀 내부 툴에 굉장히 잘 맞는다. 본 프로젝트(고객대면 프로덕트)는 React 유지할 듯. 팀 스택 일관성도 중요하니까. 개인 프로젝트엔 계속 Vue 쓸 예정.

20160211

Docker Compose 운영 레시피

Compose v1.6 쓰면서 몇 달 굴려본 운영 레시피. 아직 single host 한정 (멀티노드는 swarm 보고있는중).

기본 셋업은 web + db + redis + nginx 조합이 많음. docker-compose.yml 공유.

version: '2'
services:
  web:
    build: .
    env_file: .env
    depends_on:
      - db
      - redis
    restart: unless-stopped
    volumes:
      - ./app:/app
      - logs:/app/logs
  db:
    image: mysql:5.7
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
      MYSQL_DATABASE: app
    volumes:
      - db-data:/var/lib/mysql
  redis:
    image: redis:3.0-alpine
    restart: unless-stopped
    volumes:
      - redis-data:/data
  nginx:
    image: nginx:1.9
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - web
volumes:
  db-data:
  redis-data:
  logs:

몇 가지 팁:

1. version: '2' 로 올리면 networks 자동 생성. 각 서비스끼리 hostname으로 접근 가능 (web에서 db:3306). 이전 v1 의 links 방식은 deprecated.

2. restart: unless-stopped. 호스트 재부팅 후에도 자동 재시작. always는 수동으로 stop한 것까지 살리니까 우리는 unless-stopped.

3. depends_on은 start 순서만 보장함. "DB가 connection ready" 를 보장하지 않음. 그래서 web 컨테이너 entrypoint 스크립트에서 mysql 포트 열릴때까지 wait 하는 로직 넣음:

#!/bin/sh
until nc -z db 3306; do
  echo "waiting for db..."
  sleep 1
done
exec "$@"

4. named volume 쓸 것. bind mount는 권한 이슈가 자주 생김 (컨테이너의 uid와 호스트의 uid 맞아야 함).

5. env 관리. 민감값은 env_file로. .env는 git 미포함, .env.sample만 올림.

6. 로그. 운영에서 docker-compose logs로 모든 서비스 로그 통합 조회 가능. 개발 환경에선 -f 로 tail 걸어놓음.

한계는 단일 호스트라는 점. 다음 스텝은 swarm mode 또는 k8s인데 둘 다 운영 경험 없어서 학습 시간 필요.

20160114

Webpack 1에서 2 마이그레이션 노트

Webpack 2 베타로 마이그 시도. 프로젝트 하나를 2 베타에 올려봤다. 아직 GA 아니라 레퍼런스가 많진 않아서 정리.

가장 큰 변화는 ES6 모듈 네이티브 지원. import / export를 webpack이 이해하고 tree shaking 가능하게 됨. 그리고 System.import (다이나믹 임포트) 문법 도입. 라우트 기반 코드 스플리팅에 유용.

기존 1.x config에서 가장 자주 걸리는 수정:

// 1.x
module.exports = {
  module: {
    loaders: [
      { test: /\.js$/, loader: 'babel', query: { presets: ['es2015'] } }
    ]
  },
  resolve: { extensions: ['', '.js', '.jsx'] }
};

// 2.x
module.exports = {
  module: {
    rules: [
      { test: /\.js$/, use: {
          loader: 'babel-loader',
          options: { presets: ['es2015'] }
      }}
    ]
  },
  resolve: { extensions: ['.js', '.jsx'] }  // '' 빠짐
};

체크포인트:

  • loadersrules. 용어 정리됨
  • loader: 'babel' 축약표기 제거. 풀네임 babel-loader 써야함
  • queryoptions
  • resolve.extensions에서 빈 문자열 제거
  • DedupePlugin 제거. tree shaking + UglifyJS로 대체

tree shaking은 아직 효과가 제한적. Babel이 import를 require로 바꿔버리면 webpack이 해석을 못 해서 dead code 못 찾음. .babelrc"modules": false 명시 필요:

{ "presets": [["es2015", { "modules": false }]] }

베타라 문서가 안 따라오고 커뮤니티 지식도 적어서 삽질 꽤 함. 정식 나오려면 좀 더 기다려야 할듯. 그래도 dynamic import 쪽은 미리 적용하고 싶어서 계속 테스트 진행중.