[패캠] The RED : 김민태의 React와 Redux로 구현하는 아키텍처와 리스크 관리
Web Frontend Developer

[패캠] The RED : 김민태의 React와 Redux로 구현하는 아키텍처와 리스크 관리

오늘은 패스트캠퍼스에서 최근에 수강했던 김민태님의 <The RED : 김민태의 React와 Redux로 구현하는 아키텍처와 리스크 관리> 강의를 듣고 학습했던 내용을 정리해 보려고 한다. 시작하기에 앞서서 이 강의는 패캠에서 어떠한 대가도 제공받지 않고 직접 수강하고 내용을 정리하는 것임을 밝힌다.

목차

1. 프론트엔드 개발자가 갖춰야 할 필수 소프트 스킬

  • 한 회사에 종속된 기술을 사용하는 것은 위험하다. e.g. Flash
  • 개발자가 개발만 잘 한다고 좋은 제품이 나오는 것은 아니구나. e.g. 모바일 서비스
  • 어떻게 하면 기술을 쉽게 이해할 수 있을까? e.g. 외계어 스터디

WEB

  • 개방형 스탠다드
    • 웹을 제외하고는 벤더 디펜던시가 있다(iOS, Android, Java-Spring 등)
    • 웹도 지금 100% 개방형 기술이라고 보기는 어렵다.

FRONT

  • 제일 앞에 있다.
  • 시각적 요소 중요

END

  • 프론트엔드가 만들어지면 제품이 완성
  • 제품의 가장 마지막 단계
    • 언제 출시할꺼야?

전략

  1. 웹 프론트엔드 개발자라면 알아야 하는 지식
    1. 네트워크
    2. 메모리
    3. 퍼포먼스
  2. 필요한 상황이 생기면 알고 싶지만 쉽게 배울 수 없는 지식
    1. 그래픽스
    2. 수학
  3. 민태님이 공부하는 분야
    1. web assembly

2. 안정적인 프로덕트를 위한 아키텍처 설계와 리스크 관리

프로덕트 개발환경

문제는 어디서 발생하는가?

  • 일정 규모 이상의 성장을 시작할 때 (Scale)

스타트업

  • 제한된 리소스
    • 인적 자원
      • 나와 동료
      • 부족한 시간
    • 역량의 한계를 드러내기 시작하라
    • 리스크를 줄이기 위한 노력
      1. 필요없는 기능 배제하라
      2. 클린 코드에 집착하지 말아라. 동작하는 코드가 가장 가치있는 시기가 있다.
    • MVP
      • 서비스 관점의 MVP
        • 접근성.. SEO... 아키텍처...
        1. 개발자가 판단한 MVP가 정답인지 의심.
        2. 직관이 통하는 세계가 존재함을 인정. 직관은 과학이 아님.
        3. 개발자의 역할은 직관의 실패 리스크를 최소화하며 피봇할 수 있게 지원하는 것.
      • 기술 관점의 MVP
        1. 어떤 기술이 적정기술인가?
        2. 최대한 포기하라, 포기할 것과 포기하지 못하는 것을 분류하고 검토하라
        3. 기술보다 중요한 건 속도다. 언제나 서비스의 생존이 최우선이다.
  • 시니어의 부재
  • 실패 비용

웹 개발을 설계하는 방식

  1. 웹의 철학과 특징을 고려하라
    1. 소스를 모두 오픈한다.
    2. 어떠한 환경에서도 동일한 컨텐츠를 전달해야 한다.(크로스 브라우징)
  2. 기술이 서비스 성공의 촉매 역할을 할 수 있다.
    1. ex. 접근성, SEO, 위트 등
  3. 모든 것이 공유될 수 있는 자원이라는 것을 고려하라.
    1. 검색엔진 최적화, 오픈 그래프 최적화, 소스 코드
  4. 외부 서비스 연동 정보를 관리하라
    1. API Key, 인증서 등
  5. 신규 서비스의 어설퍼 보이는 UX는 좋은 이미지를 만들 수 없고, 가치가 하락한다.
  6. 발빠른 테스트와 릴리즈를 위한 아키텍처를 처음부터 고려하라
    1. ex. A/B 테스트, 부분 업데이트가 가능한 격리된 컴포넌트 구조 설계
  7. UX의 견고함과 기능이 경합을 벌이면 견고함을 선택하라
  8. 레거시가 없는 서비스라는 장점을 최대한 활용하라. 적절한 최신 기술 사용은 매력 요소가 별로 없는 스타트업의 기술인력 확보에 도움을 줄 수 있다.
    1. ex. PWA, WebRTC, AMP, Web Assembly 등
  9. 낮은 버전의 브라우저 환경은 과감히 버려라
  10. 사용자 로그를 수집하라. 그리고 분석하라
    1. 로그는 필수 요소이다. 분석 인프라가 없더라도 초기부터 로그를 수집하라
    2. 로그 분석 인프라를 마련하고 지속적으로 발전시켜라

웹뷰 개발을 설계하는 방식

  • 네이티브 앱 패키징 아키텍처
    • 네이티브 컨테이너 + 단일 웹뷰
      • 앱스토어 규약 위반 가능성, 네이티브 기능을 탑재해야 함
      • ex. 푸시, 카메라, 네이티브 인증(페이스 타임, 지문 인식 등)
    • 네이티브 + 멀티 웹뷰
      • 웹뷰간 데이터 교환 방법을 초기부터 고안해야 함
      • 앱에 저장소를 만들고 웹뷰에게 인터페이스를 제공하라
      • ex. 결제, 주문 상세, 주문 목록
    • 네이티브 + RN
      • 변화가 많은 지면은 RN으로 개발, 그 외의 지면은 네이티브 개발
      • 역량있는 네이티브 개발자가 필요하다.
  • 앱 패키징 아키텍처와 무관한 고려 사항
    • 네비게이션 룰을 확립하라
      • 특정 화면의 직접 랜딩을 위한 앱스킴 디자인
      • 사용자가 다이렉트로 접근할 수 있는 경로가 반드시 필요하다(deeplink)
      • 향후 웹 서비스 만들꺼야 → universial link를 deeplink의 기본 스펙으로
    • 공개용 웹뷰와 내부용 웹뷰를 분리하라
    • 보안을 고려하라
      • API 연동 토큰 및 앱 메타 정보 등 서버가 필요로 하는 정보를 어떻게 관리할지 고려하라
    • 개발 환경을 구축하라
      • 웹뷰와 데스크탑 브라우저는 다르다.
      • 같은 기기 내 웹뷰, 브라우저 등도 차이점이 존재한다.
      • 시뮬레이터와 실기기에서 작동하는 것도 차이가 존재한다.
      • 각각에 대해 개발자가 경험할 수 있는 환경을 미리 준비하고 개발자간 쉽게 방법을 공유할 수 있도록 문서화하고 변경사항을 업데이트한다.
    • 런타임 오류를 수집하라
      • 디바이스 환경에 브라우저 환경보다 훨씬 더 다양하다
      • AOS >>> iOS
    • 캐싱을 적극적으로 활용하라
      • 캐싱된 데이터와 서버 패치 데이터의 자연스러운 전환을 고려한 UX를 설계하라
      • 네트워크가 안 될 때 사용자에게 보여줄 수 있는 데이터를 캐싱하라.
    • 웹뷰의 라이프사이클을 인지할 수 있는 인터페이스를 설계하라
      • 2번 탭 - 웹뷰, 3번 탭 - 네이티브
      • 2번 탭 → 3번 탭 → 2번 탭 : 최신 정보를 패치하려면?
    • 프리젠테이션 컴포넌트를 독립적으로 운영하라
    • 접근성을 언제나 고려하라

신규 개발 관점에서의 리스크 관리

  • Communication
  • 프로토타입
    • API가 없어도, 디자인이 없어도 프로토타입은 가능
    • 돌아가는 걸 보여주어라. 그래야 더 설득력이 있고 명확하다.
    • 프로토타입은 PoC와 다르다.
  • Mock API
    • 미리 준비해서 일정을 최대한 맞출 것
    • 어짜피 예측하지 못한 변수들은 반드시 생긴다.

유지, 개선 관점에서의 리스크 관리

  • 레거시 코드를 존중하자.
    • 모든 코드는 릴리즈 되는 순간 레거시 코드다.
    • 서비스를 생존시킨 레거시 코드는 존중받아 마땅하다.
    • 비난은 아무런 이익을 만들어 내지 못한다.
  • 시각화되지 않은 문제는 불만일 뿐이다.
  • 레거시 코드는 왜 분석하기가 힘든가?
    1. 기술적인 난이도가 높다.
      1. 역략 한계를 인정하고 공개한 후 함께 해결책을 찾아라.
    2. 잘못된 구조로 규모가 커진 코드

안정적인 프로덕트를 위한 코드 구조와 관리

  • 유형 → 뒤섞이면 문제가 발생한다.
    • HTML
    • CSS
    • Code/Logic/Rule
    • Domain/Data/State
    • Effect
  • 변형 주기
    • Design/Visual/Struct
    • Config
      • 분리가 안 되어 있으면 서버가 바뀔 때마다 코드가 새로 배포되어야 한다.
    • Assets/Information
      • 약관 같은 것들도 Information에 포함이 된다.
  • 오너십
    • Library
    • Framework
    • Service
  • 위치

3. React 와 Redux로 구현하는 아키텍처와 리스크 관리

리액트로 구현하는 아키텍처와 리스크 관리법

// /src/index.js
import { createElement, render, Component } from './react.js';

class YourTitle extends Component {
    render() {
        return (
            <p>Hello Title</p>
        )
    }
}

function Title() {
    return (
        <div>
            <h2>정말 동작 할까?</h2>
            <YourTitle />
        </div>
    );
}

render(<Title />, document.querySelector('#root'));
// /src/react.js

const hooks = [];
let currentComponent = 0;

export class Component {
  constructor(props) {
    this.props = props;
  }
}

// react hook
function useState(initValue) {
    const position = currentPosition;

    if (!hooks[position]) {
        hooks[position] = initValue;
    }

    return [
        hooks[position],
        (nextValue) => {
            value = nextValue
        }
    ];
}

// 객체인 vdom을 받아 리얼 dom으로 만들어 주는 함수
function renderElement(node) {
  if (typeof node === "string") {
    return document.createTextNode(node);
  }

  if (node === undefined) return; // stackoverflow 방지

  const $el = document.createElement(node.type);

  node.children.map(renderElement).forEach((node) => {
    $el.appendChild(node);
  });

  return $el;
}

// render 함수
export const render = (function() {
    let prevVdom = null;

    return function(nextVdom, container) {
        if (prevVdom === null) {
            prevVdom = nextVdom;
        }
        container.appendChild(renderElement(vdom));
    };
})();

// jsx를 안 쓰고 리액트를 만들 때 사용하는 함수, vdom을 만듦
export function createElement(type, props, ...children) {
  if (typeof type === "function") {
    if (type.prototype instanceof Component) { // Class Component
      const instance = new type({ ...props, children });
      return instance.render.call(instance); // 함수 호출 횟수와 상관없이 항상 동일한 순서로 호출되어야 함
    } else { // Functional Component
      currentComponent++;
            return type.apply(null, [props, ...children]); // 가변인자를 쭉 넘겨주려면 apply 메서드를 사용하자.
    }
  }
  return { type, props, children };
}

Redux를 통한 실전 리스크 관리법

// ./app.js

import { createStore, actionCreator } from "./redux-middleware";

function reducer(state = {}, { type, payload }) {
  switch (type) {
    case "init":
      return {
        ...state,
        count: payload.count
      };
    case "inc":
      return {
        ...state,
        count: state.count + 1
      };
    case "reset":
      return {
        ...state,
        count: 0
      };
    default:
      return { ...state };
  }
}

// currying
const logger = (store) => (next) => (action) => {
  console.log("logger: ", action.type);
  next(action);
};

const monitor = (store) => (next) => (action) => {
  setTimeout(() => {
    console.log("monitor: ", action.type);
    next(action);
  }, 2000);
};

const store = createStore(reducer, [logger, monitor]);

store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch({
  type: "init",
  payload: {
    count: 1
  }
});

store.dispatch({
  type: "inc"
});

const Reset = () => store.dispatch(actionCreator("reset"));
const Increment = () => store.dispatch(actionCreator("inc"));

Increment();
Reset();
Increment();
// ./redux-middleware.js

export function createStore(reducer, middlewares = []) {
  let state;
  const listeners = [];
  const publish = () => {
    listeners.forEach(({ subscriber, context }) => {
      subscriber.call(context);
    });
  };

  const dispatch = (action) => {
    state = reducer(state, action);
    publish();
  };

  const subscribe = (subscriber, context = null) => {
    listeners.push({
      subscriber,
      context
    });
  };

  const getState = () => ({ ...state });
  const store = {
    dispatch,
    getState,
    subscribe
  };

  middlewares = Array.from(middlewares).reverse();
  let lastDispatch = store.dispatch;

  middlewares.forEach((middleware) => {
    lastDispatch = middleware(store)(lastDispatch);
  });

  return { ...store, dispatch: lastDispatch };
}

export const actionCreator = (type, payload = {}) => ({
  type,
  payload: { ...payload }
});

4. Special.Student Session

프로젝트에 마인드맵을 작성해 보는 건 좋은 습관인 것 같다.

  • 라이브러리를 쓸 때 너무 업데이트가 안 되면 사용을 경계.
  • 컴포넌트의 역할을 확실하게 구분을 할 것(Presentional vs Container, View vs Logic)
  • 컴포넌트를 처음부터 너무 잘게 쪼개는 것은 경계
  • 소비하는 컴포넌트와 소비를 위해 제공하는 컴포넌트간에 의존성이 너무 강하면 복잡도 증가.
  • 네트워크 관련 에러 처리를 꼼꼼하게 해서 사용자에게 일관된 경험을 주어야 한다.
  • 클라이언트 - 대규모 트래픽 처리 관련 -> 캐싱, 이전 데이터를 먼저 불러오고 새로운 fetch를 background에서 불러오게 하는 전략