레이블이 Frontend인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Frontend인 게시물을 표시합니다. 모든 게시물 표시

20210523

Vite 2 번들러 교체

CRA 기반 앱을 Vite 2로 교체했다. 2월에 2.0 나오고 지금은 2.3 정도. webpack 대비 체감은 "시동이 빠르다" 정도가 아니라 "이게 왜 가능하지" 수준.

왜 빠른가

dev 서버가 번들링을 안 한다. 브라우저가 ESM을 직접 요청하고 Vite는 요청된 파일을 온더플라이로 트랜스폼. 결과: 초기 dev server 구동이 1초 안쪽으로 떨어짐. 우리 프로젝트 기준 웹팩 CRA는 약 28초 걸림.

설정

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: { port: 3000, proxy: { '/api': 'http://localhost:8080' } },
  build: { outDir: 'build', sourcemap: true }
})

마이그레이션 시 걸린 것

  • process.env.REACT_APP_*import.meta.env.VITE_*. 변수 이름 전부 리네임.
  • webpack alias — resolve.alias로 그대로 옮기면 됨.
  • CommonJS 전용 라이브러리 몇 개가 import로 안 풀림. optimizeDeps.include에 넣어서 pre-bundle 유도.
  • svg import — @svgr/plugin-*로 플러그인 세팅 따로.
dev는 ESM이지만 prod build는 Rollup 기반. 런타임 차이가 생길 수 있다. 프로덕션 빌드로 E2E 한 번 꼭 돌려봐야 함.

결과

  • dev start: 28s → 0.9s
  • HMR: 평균 1.5s → 50ms 수준
  • prod build: 1m 40s → 55s

지금까진 만족. 단 generic한 CLI 경험은 webpack이 훨씬 레거시 친화적이라서, 오래된 앱이면 바로 옮기진 말고 PoC 먼저 하는 게 낫다.

20201020

React 17 — 변화보다 업그레이드 루틴

React 17 정식 나왔다. 공식 블로그 제목부터 "No New Features". 동의한다. 유저 페이싱 기능 변화는 거의 없음. 대신 업그레이드 루틴을 정리하고 18 준비(concurrent)를 위한 토대를 닦는 릴리스. 실제 작업한 기록.

실제 중요한 변화

1. 이벤트 위임 타겟이 document → root. 16까지는 모든 합성 이벤트(onClick 등)가 document에 붙어 있었다. 이게 여러 React 트리가 한 페이지에 공존할 때(마이크로프론트엔드, 위젯 삽입) 이벤트 가로채기 순서가 꼬이는 원인. 17은 ReactDOM.render(el, rootEl)의 rootEl에 이벤트가 붙는다.

실제 이득: 레거시 코드(jQuery)와 React가 섞인 페이지에서, jQuery에서 e.stopPropagation()했는데도 React가 잡아버리던 현상이 사라진다. 또 17 + 16 트리를 한 페이지에 섞어 돌리는 "부분 업그레이드"가 가능해진다. 우리는 레거시 관리자 내부에 일부 페이지만 React 화했던 적이 있어서 이 변경이 반갑다.

2. 새 JSX 트랜스폼. 16까지는 컴포넌트 파일마다 import React from 'react'가 필수(JSX가 React.createElement로 변환되므로). 17 + Babel 7.9(@babel/preset-react 7.9.0 이상) 또는 TS 4.1부터 automatic runtime 지원. 빌드 타임에 필요한 함수만 자동 import.

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-react', { runtime: 'automatic' }]
  ]
}

번들 사이즈 미세 감소(프로젝트 크기에 비례해서 몇 KB). 더 큰 건 개발 편의. 우리 레포에 import React from 'react'가 800번 나오던 거 전부 codemod로 제거 가능.

npx react-codemod update-react-imports

3. useEffect cleanup 타이밍. 16까지는 언마운트 시 cleanup이 동기적으로 실행 → 화면이 잠깐 잠김. 17부터는 렌더 이후 비동기로. 무거운 cleanup(구독 해제, 타이머 정리)이 UI를 막지 않음. 대신 cleanup 완료 전에 다음 렌더가 일어날 수 있으므로 cleanup 내부에서 mutate하는 코드는 다시 한번 확인.

4. 일관된 error handling. componentDidMount/componentDidUpdate의 에러가 unmount를 강제하도록 변경. 이전에는 에러 후에도 컴포넌트가 일부 남아서 crash가 퍼지는 경우 있었음.

업그레이드 과정

우리 프로젝트는 react-scripts 3.x(CRA)였고 17 쓰려면 react-scripts 4.x로 동반 올림. eslint, babel, webpack, postcss 쪽 peer dep이 쫙 바뀐다.

npm i react@17 react-dom@17 react-scripts@4
# peer dep 경고가 과격하면
npm i --legacy-peer-deps

그 외 실수하기 쉬운 포인트.

  • enzyme 어댑터가 17 대응이 늦어서 문제. 공식 enzyme-adapter-react-17이 아직 unofficial. 우리는 이 기회에 @testing-library/react로 전환 시작. 테스트 코드 꽤 많이 갈아엎어야 함
  • 사용하던 외부 라이브러리 peer deps: react-router 5.x OK, redux/react-redux 7.2 OK, material-ui 4.x OK(5는 별도 마이그 계획). react-dnd는 11 이상 필요
  • 클래스 컴포넌트에서 UNSAFE_componentWillMount 사용 중이면 경고. strict mode면 더 요란함. 이 기회에 리팩터

개발 경험 변화

  • HMR(react-refresh)은 react-scripts 4에서 기본. fast-refresh가 안정적이고 상태 보존이 좋다
  • Strict Mode 이중 렌더링: 18 concurrent 준비로 effect의 순수성을 확인하는 진단 모드. 17도 영향 있으니 개발 환경에서 켜서 잡아내는 게 좋다
  • TypeScript 4.1 + automatic JSX runtime이 깔끔. tsconfig의 "jsx": "react-jsx"

성능 실측

우리 admin SPA(Redux + react-router, 120개 라우트) 기준.

  • 초기 번들: 412KB → 408KB (automatic jsx 효과로 -4KB)
  • TTI(로컬): 1.92s → 1.88s. 유의미한 차이 아님. "기능은 없다"는 공식 발표 그대로
  • HMR 반영 속도: 1.4s → 0.9s(react-refresh 적용 효과)

결론

17은 18(concurrent features) 가는 정거장. 지금 업그레이드 해둬야 18 전환 비용이 줄어든다. 체감은 애매하지만 "ImportReact 전부 제거", "enzyme → RTL 전환", "Strict Mode 켜서 잠재 버그 훑어보기" 이 세 가지를 할 명분이 생기는 게 크다. 팀 코드베이스 정화 핑계로 삼자.

남은 숙제: webpack 4 → 5, material-ui 4 → 5 베타 둘 다 큰 건이라 다음 분기로 밀어둠.

20181027

React Hooks 알파 — 리팩터링 계획

React Conf에서 Hooks 공개됐다. 16.7 alpha에 들어있음. 일단 사이드 프로젝트에 붙여봤는데, 이게 class 문법 없이 상태 가진 컴포넌트를 만드는 거라 코드 모양이 확 바뀐다.

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

function Counter() {
  const [n, setN] = useState(0);
  useEffect(() => {
    document.title = `count ${n}`;
  }, [n]);
  return (
    <button onClick={() => setN(n + 1)}>+{n}</button>
  );
}

이거 실화냐... lifecycle 뭘로 쪼개서 썼나 싶었던 코드들이 다 정리됨. componentDidMount + componentDidUpdate로 중복 써야 했던 fetch 로직이 useEffect 하나로 합쳐지는 게 크다.

리팩터 계획 메모:

  • 순수하게 상태/effect만 쓰는 함수 컴포넌트는 언제든 hooks로. 빠르게 전환 가능
  • HOC로 감싸던 withAuth, withTheme → custom hook(useAuth, useTheme)으로 교체 검토
  • class 컴포넌트에서 라이프사이클 복잡하게 얽힌 것(ex. resize + subscription + timer)은 단계별로
  • redux connect 쓰는 건 일단 놔두기. react-redux 쪽 hooks 버전 기다림

아직 alpha라 prod 투입은 당연히 안 함. 근데 API 자체는 거의 확정된 느낌이라 지금 써봐도 학습 가치가 있다. 내년 초엔 stable 올 듯.

20180916

PWA lighthouse 점수 끌어올리기

Lighthouse 3.0이 Chrome 69 DevTools에 번들로 들어오면서 점수 체계가 꽤 빡빡해졌다. 사내 PWA 프로젝트 점수가 갑자기 떨어져서 원인 분석 + 개선한 기록. 2.x 시절 80점 찍던 앱이 3.0에선 65까지 떨어진다. 이게 버그가 아니라 기준 변화.

현재 상태 (개선 전)

  • Performance 71
  • Accessibility 88
  • Best Practices 93
  • PWA 77
  • SEO 90

Lighthouse 3.0에서 Performance 산출식이 바뀌었다. FCP, FMP, Speed Index, TTI, First CPU Idle, Estimated Input Latency — 이 6개의 가중 평균인데, 3.0부터 TTI와 Speed Index 가중치가 커졌다. 우리 앱은 initial JS 번들이 큰 편이라 TTI가 밀려서 점수 하락.

고친 것들 순서대로

1) manifest.json에 background_color 누락. 이게 없으면 Android 홈에서 앱 런치 시 splash screen이 안 뜨고, Lighthouse PWA 검사에서 "Splash screen" 항목이 감점. 이 한 줄 추가로 PWA 총점 +6.

{
  "name": "Reindeers Admin",
  "short_name": "RDAdmin",
  "start_url": "/?utm_source=homescreen",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0e47a1",
  "icons": [
    { "src": "/static/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/static/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
  ]
}

icons는 192와 512 둘 다 필수. purpose: "maskable"은 안드로이드 O 이상에서 아이콘 모양 테마 대응(safe zone 안에 로고 배치). 이 속성 모르는 팀이 많음.

2) Service Worker 등록 경로와 scope. 기본적으로 /sw.js는 최상위에 있어야 scope가 사이트 전체("/")로 잡힌다. 우리는 정적 파일 디렉터리 구조상 /static/sw.js에 있었는데 이 경우 기본 scope가 /static/이 되어 루트 경로들을 제어 못 함. 두 가지 해법이 있다.

  1. 빌드 산출물을 루트로 복사(또는 nginx location = /sw.js로 별칭)
  2. Service-Worker-Allowed 헤더로 scope 확장
# nginx
location = /sw.js {
    alias /var/www/app/static/sw.js;
    add_header Service-Worker-Allowed "/" always;
    add_header Cache-Control "no-cache" always;
}

Cache-Control도 중요. sw.js는 반드시 max-age 0 혹은 no-cache. sw는 변경되지 않으면 브라우저가 새 버전을 감지 못하고, 감지 경로가 이 파일 자체의 HTTP fetch 결과 diff라서 긴 캐시가 걸리면 업데이트 절대 안 내려간다.

3) offline fallback. 네트워크 죽었을 때 뭐라도 보여야 PWA "Works offline" 체크 통과. 가장 간단한 네트워크 우선 + 오프라인 fallback.

const RUNTIME_CACHE = 'rt-v3';
const OFFLINE_URL = '/offline.html';
const PRECACHE = ['/offline.html', '/static/icons/icon-192.png'];

self.addEventListener('install', e => {
  e.waitUntil(caches.open('core-v3').then(c => c.addAll(PRECACHE)));
  self.skipWaiting();
});

self.addEventListener('activate', e => {
  e.waitUntil(self.clients.claim());
});

self.addEventListener('fetch', e => {
  if (e.request.mode === 'navigate') {
    // 네트워크 우선, 실패 시 오프라인 페이지
    e.respondWith(
      fetch(e.request).catch(() => caches.match(OFFLINE_URL))
    );
    return;
  }
  // static 리소스는 stale-while-revalidate
  e.respondWith(
    caches.match(e.request).then(cached => {
      const fetchP = fetch(e.request).then(res => {
        const clone = res.clone();
        caches.open(RUNTIME_CACHE).then(c => c.put(e.request, clone));
        return res;
      }).catch(() => cached);
      return cached || fetchP;
    })
  );
});

navigate 요청(HTML)과 정적 리소스 전략을 분리. 네비게이션 실패는 오프라인 페이지, 정적 리소스는 stale-while-revalidate로 즉시 응답 + 백그라운드 갱신.

4) 이미지 lazy load — IntersectionObserver로 교체. 기존에는 scroll 이벤트에서 getBoundingClientRect로 체크하는 관용구. 이게 메인 스레드 점유가 쏠쏠했다. IntersectionObserver로 바꾸니 스크롤 이벤트 자체가 제거되고 TTI 800ms 감소. long task가 거의 사라졌다.

const io = new IntersectionObserver(entries => {
  for (const e of entries) {
    if (!e.isIntersecting) continue;
    const img = e.target;
    img.src = img.dataset.src;
    if (img.dataset.srcset) img.srcset = img.dataset.srcset;
    io.unobserve(img);
  }
}, { rootMargin: '100px' });

document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));

5) initial JS 번들 쪼개기. webpack 4의 splitChunks로 vendor/common/app 3분리. 라우트별 dynamic import(import('./page/orders'))로 관리자 권한 페이지를 떼어냄. 메인 route 초기 JS가 420KB → 180KB(parsed). TTI가 또 1.1초 빠짐.

6) 폰트 FOIT 제거. font-display: swap 선언과 <link rel="preload" as="font" crossorigin> 조합. FCP가 240ms 개선.

결과

  • Performance 71 → 91
  • Accessibility 88 → 95 (aria-label, color contrast 몇 개)
  • Best Practices 93 → 100
  • PWA 77 → 97
  • SEO 90 → 100 (meta description, hreflang)

남은 3점은 "maskable icon purpose" 속성이 Lighthouse 3.0 기준 아직 flaky하게 감지됨. 재검사하면 올라오기도 함. 미세한 문제라 일단 두기로.

교훈

  • Lighthouse 버전 올라갈 때마다 기준이 바뀐다. 점수 변화를 "회귀"로 오인하지 말 것
  • Performance는 JS 크기와 메인 스레드 점유가 거의 전부. Service Worker로 요리조리 캐시한다고 TTI가 극적으로 오지는 않는다
  • DevTools의 Coverage 탭으로 실제로 안 쓰는 코드 비율 확인. 대부분 40~60%가 첫 페이지에 미사용
  • CI에 lighthouse-ci 붙여서 PR마다 감시하면 회귀 예방에 효과적. 점수 임계값 넘으면 fail

20170723

React 16 Fiber 출시 후 리팩토링

React 16 beta 찍먹. 커뮤니티에서 말 많던 Fiber가 드디어 기본 렌더러. 기존 코드가 얼마나 그대로 도는지 확인해봄.

결론: 대부분 그대로 동작. 몇 가지만 손보면 됨.

바뀌는 점 (주의할 것):

  • render()에서 배열 / 문자열 / 숫자 / null 직접 반환 가능. 예전엔 div로 감싸야 했던거
  • 에러 경계 (componentDidCatch) 도입. 트리 내 어디서 throw 나도 fallback UI로 대체 가능
  • Fragment — <React.Fragment>...</React.Fragment> 로 무의미 div 제거
  • Portal — 특정 DOM 노드 밖으로 렌더 (모달, 툴팁)
  • PropTypes, React.createClass 는 별도 패키지로 분리. 안 바꾸면 경고 뜸
  • setState에 null 반환하면 업데이트 스킵됨 (noop)

Fiber 자체는 "기존 stack 기반 reconciler를 교체" 한 거라 use-site에선 티 안 남. 내부적으로 렌더를 중단/재개 가능하게 쪼개놓은 구조. time slicing이 가능해지는 기반이지만 16에선 아직 기본 활성은 아님. 그건 앞으로 (async mode) 점진 도입.

리팩토링 체크리스트 (팀에 공유한 거):

1. PropTypes import 경로 확인 — import PropTypes from 'prop-types'; 로 바꿈
2. React.createClass 쓰는 오래된 파일 있나 검색 — ES6 class로 교체
3. render return 에 무의미 wrapper div 있으면 Fragment로
4. 앱 루트에 에러 경계 하나 붙여두기. 컴포넌트 하나 터져서 흰 화면 안 되도록

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  componentDidCatch(error, info) {
    this.setState({ hasError: true });
    logger.error(error, info);
  }
  render() {
    if (this.state.hasError) return <FallbackUI/>;
    return this.props.children;
  }
}

번들 사이즈도 조금 줄음. 체감 rendering 성능은 일반 UI에선 아직 큰 차이 없는데, 대규모 트리 업데이트 상황에서 dropped frames가 줄어든 것이 측정됨. 나중에 async mode 켜지면 이게 더 극적으로 좋아질 것 같다.

16은 마이너 브레이킹만 잘 잡으면 안전하게 올릴 수 있는 버전. 회사 주력 프로덕트는 다음달 적용 예정.

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로 분해)은 하지 않도록. 이 습관이 리팩토링 시간 꽤 줄여줌.

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월 예정. 꼭 써봐야 할 플래그.

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 쓸 예정.

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 쪽은 미리 적용하고 싶어서 계속 테스트 진행중.

20150813

React 0.14 splitting 패키지 정리

React 0.14 beta 돌면서 가장 큰 변화가 react와 react-dom 패키지 분리. npm에서 따로 받아야 함.

npm install --save react@0.14.0-beta3 react-dom@0.14.0-beta3

기존:

var React = require('react');
React.render(<App/>, document.getElementById('root'));

0.14:

var React = require('react');
var ReactDOM = require('react-dom');
ReactDOM.render(<App/>, document.getElementById('root'));

왜 쪼갰냐? 서버사이드(Node)랑 클라이언트(브라우저)에서 쓰는 부분이 달라서. React Native 등장으로 DOM에 묶인 코드를 코어에서 분리하는 흐름. 앞으로 react-art, react-canvas 같은 렌더러들이 같은 방식으로 붙을 것.

바뀐 것들 정리:

  • React.findDOMNodeReactDOM.findDOMNode
  • React.unmountComponentAtNodeReactDOM.unmountComponentAtNode
  • PropTypes 여전히 React에 있음 (아직)
  • stateless functional component 처음 지원. const MyComp = (props) => <div>{props.name}</div>
  • ref callback 형태 등장. string ref는 deprecated 예정

우리 회사 프로젝트는 0.13에서 쓰던게 꽤 있는데 migration 다 해봤다. 거의 찾아바꾸기 수준. stateless functional component는 앞으로 dumb component 쪽에 적극 도입할 계획. 성능상 최적화 여지도 있고 (추후 버전에서 별도 optimization 예정이라고 함).

이번 release는 중간 정리 성격. 0.13에서 0.14 넘어가는 것 자체는 크게 고통스럽진 않았음.

20140203

Bootstrap 3 반응형 그리드 실전

Bootstrap 3 갈아타면서 반응형 그리드 잡아봄. 2에서 올라오면 가장 크게 걸리는게 클래스 이름이 span*col-md-* 로 바뀐거. 그리고 breakpoint가 xs/sm/md/lg 로 나눠짐.

대충 이런 식으로 만들면 모바일에선 1칼럼, 태블릿 2, 데스크톱 3이 됨.

<div class="row">
  <div class="col-xs-12 col-sm-6 col-md-4">카드 1</div>
  <div class="col-xs-12 col-sm-6 col-md-4">카드 2</div>
  <div class="col-xs-12 col-sm-6 col-md-4">카드 3</div>
</div>

주의:

  • xs에서 col 안 주면 기본이 block이라 전체 너비 차지. 그걸 의도했으면 빼도 되는데 명시하는게 좋음
  • col 합이 12 넘어가면 그 다음 col이 자동으로 다음 줄로 내려감. 행 높이 다르면 여백이 요철처럼 생기는데 .clearfix + visible-* 로 잡거나 equal height 플러그인 써야 함
  • 태블릿 두 개 나열하고 PC에서 세 개 할때 3번째에서 .clearfix.visible-sm-block 넣어야 레이아웃 안 깨짐

sm 이하에서 메뉴 접는 네비바는 .navbar-collapse 만 붙이면 자동으로 되는데, 회사 내부 관리자페이지 만들때는 굳이 반응형 안 해도 되는 경우가 많으니 오버엔지니어링 조심.

less 소스 가져다 xs/sm/md/lg 브레이크포인트 숫자만 우리 회사 레이아웃에 맞게 바꿔서 쓰는 중. 기본은 768/992/1200 이지만 우리는 디자인 기준이 990이라 미세조정.

20131214

jQuery 1.10 vs 1.11 호환성 체크

서비스 jQuery 1.10.2 → 1.11.0 올려도 되나 싶어 체크한 기록. 결론부터: 거의 문제없음. 다만 자체 플러그인 두 개를 손봐야 했다.

먼저 라인 선택 배경. 2.x로 못 가는 이유는 IE8. 회사 고객 중 공공/대기업 쪽에 IE8이 아직 6% 남아있어서 1.x 유지는 강제. 1.11은 bugfix 중심이고 API 브레이킹이 없다고 공식 changelog에 박혀 있다. 2.1은 기능적으로는 1.11과 같지만 IE<9 지원 코드가 제거된 버전. 같은 릴리스 사이클로 번호만 다르게 간다고 보면 됨.

실제 체크한 항목.

  • $.ajaxstatusCode 콜백 분기 — 동일
  • $.Deferred + .then 체이닝 — 동일. 1.8에 있던 then 리턴값 체이닝 버그는 이미 고쳐진 상태
  • 이벤트 delegation .on("click", "a.btn", fn) — OK
  • 오래된 $.browser 사용 플러그인(slimScroll 0.4) — 여전히 미동작. 1.9부터 제거된 API라 그대로
  • $.live / $.die — 1.9에서 이미 제거. 영향 없음
  • .attr("checked") vs .prop("checked") — 폼 체크박스 상태는 반드시 prop. 1.6 이후 고정인데 코드 리뷰하다 아직 attr 쓰는 부분 발견해서 같이 정리

걸린 건 자체 제작한 IE7 호환 플러그인 두 개. 하나가 $.support.boxModel을 참조했는데 이게 1.8에서 이미 deprecated, 1.9부터 빠진 프로퍼티다. 어쩌다 아직 살아있었는지 모르겠는데 1.10에서는 undefined라 기본값 true로 동작하다가 1.11 올리고 minify된 번들에서 조건문 분기가 꼬였다.

// 예전
if (!$.support.boxModel) {
    el.width(el.width() + padLR + borderLR);
}

// 요즘은 document.compatMode로
// Quirks 모드일 때만 IE5 식 box-sizing이 적용됨
if (document.compatMode !== "CSS1Compat") {
    el.width(el.width() + padLR + borderLR);
}

그리고 또 하나. 커스텀 autocomplete에서 keydown 이벤트를 받는데 이벤트 객체의 which 대신 keyCode를 직접 읽던 부분이 있었다. 1.11에서 normalization 타이밍이 아주 살짝 달라져서 특정 IME 조합 상태일 때 한 번씩 keyCode가 0으로 들어오는 경우를 봤다. e.which로 바꾸니 일관성 회복.

minified 용량도 체크. 1.10.2가 92.6KB, 1.11.0이 93.5KB. CDN hit율(jQuery CDN) 올라가니까 자체 호스팅에서 CDN으로 전환하는 것까지 한 번에 해버림.

<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script>
  // fallback
  window.jQuery || document.write(
    '<script src="/assets/js/jquery-1.11.0.min.js"><\/script>'
  );
</script>

protocol-relative URL(//) 쓰는 이유는 https 페이지에서 mixed content 방지. fallback은 CDN 못 뜨면 로컬 파일. 이거 없으면 한번씩 죽는다.

테스트 돌려 별 이슈 없으면 다음주 화요일 새벽 배포 예정. prod 반영 후 에러 집계 보고 판단.