Web Frontend Developer
[패캠] The RED : 김민태의 React와 Redux로 구현하는 아키텍처와 리스크 관리
DevOwen
2022. 3. 23. 09:00
오늘은 패스트캠퍼스에서 최근에 수강했던 김민태님의 <The RED : 김민태의 React와 Redux로 구현하는 아키텍처와 리스크 관리> 강의를 듣고 학습했던 내용을 정리해 보려고 한다. 시작하기에 앞서서 이 강의는 패캠에서 어떠한 대가도 제공받지 않고 직접 수강하고 내용을 정리하는 것임을 밝힌다.
목차
1. 프론트엔드 개발자가 갖춰야 할 필수 소프트 스킬
- 한 회사에 종속된 기술을 사용하는 것은 위험하다. e.g. Flash
- 개발자가 개발만 잘 한다고 좋은 제품이 나오는 것은 아니구나. e.g. 모바일 서비스
- 어떻게 하면 기술을 쉽게 이해할 수 있을까? e.g. 외계어 스터디
WEB
- 개방형 스탠다드
- 웹을 제외하고는 벤더 디펜던시가 있다(iOS, Android, Java-Spring 등)
- 웹도 지금 100% 개방형 기술이라고 보기는 어렵다.
FRONT
- 제일 앞에 있다.
- 시각적 요소 중요
END
- 프론트엔드가 만들어지면 제품이 완성
- 제품의 가장 마지막 단계
- 언제 출시할꺼야?
전략
- 웹 프론트엔드 개발자라면 알아야 하는 지식
- 네트워크
- 메모리
- 퍼포먼스
- 필요한 상황이 생기면 알고 싶지만 쉽게 배울 수 없는 지식
- 그래픽스
- 수학
- 민태님이 공부하는 분야
- web assembly
2. 안정적인 프로덕트를 위한 아키텍처 설계와 리스크 관리
프로덕트 개발환경
문제는 어디서 발생하는가?
- 일정 규모 이상의 성장을 시작할 때 (Scale)
스타트업
- 제한된 리소스
- 인적 자원
- 나와 동료
- 부족한 시간
- 역량의 한계를 드러내기 시작하라
- 리스크를 줄이기 위한 노력
- 필요없는 기능 배제하라
- 클린 코드에 집착하지 말아라. 동작하는 코드가 가장 가치있는 시기가 있다.
- MVP
- 서비스 관점의 MVP
- 접근성.. SEO... 아키텍처...
- 개발자가 판단한 MVP가 정답인지 의심.
- 직관이 통하는 세계가 존재함을 인정. 직관은 과학이 아님.
- 개발자의 역할은 직관의 실패 리스크를 최소화하며 피봇할 수 있게 지원하는 것.
- 기술 관점의 MVP
- 어떤 기술이 적정기술인가?
- 최대한 포기하라, 포기할 것과 포기하지 못하는 것을 분류하고 검토하라
- 기술보다 중요한 건 속도다. 언제나 서비스의 생존이 최우선이다.
- 서비스 관점의 MVP
- 인적 자원
- 시니어의 부재
- 실패 비용
웹 개발을 설계하는 방식
- 웹의 철학과 특징을 고려하라
- 소스를 모두 오픈한다.
- 어떠한 환경에서도 동일한 컨텐츠를 전달해야 한다.(크로스 브라우징)
- 기술이 서비스 성공의 촉매 역할을 할 수 있다.
- ex. 접근성, SEO, 위트 등
- 모든 것이 공유될 수 있는 자원이라는 것을 고려하라.
- 검색엔진 최적화, 오픈 그래프 최적화, 소스 코드
- 외부 서비스 연동 정보를 관리하라
- API Key, 인증서 등
- 신규 서비스의 어설퍼 보이는 UX는 좋은 이미지를 만들 수 없고, 가치가 하락한다.
- 발빠른 테스트와 릴리즈를 위한 아키텍처를 처음부터 고려하라
- ex. A/B 테스트, 부분 업데이트가 가능한 격리된 컴포넌트 구조 설계
- UX의 견고함과 기능이 경합을 벌이면 견고함을 선택하라
- 레거시가 없는 서비스라는 장점을 최대한 활용하라. 적절한 최신 기술 사용은 매력 요소가 별로 없는 스타트업의 기술인력 확보에 도움을 줄 수 있다.
- ex. PWA, WebRTC, AMP, Web Assembly 등
- 낮은 버전의 브라우저 환경은 과감히 버려라
- 사용자 로그를 수집하라. 그리고 분석하라
- 로그는 필수 요소이다. 분석 인프라가 없더라도 초기부터 로그를 수집하라
- 로그 분석 인프라를 마련하고 지속적으로 발전시켜라
웹뷰 개발을 설계하는 방식
- 네이티브 앱 패키징 아키텍처
- 네이티브 컨테이너 + 단일 웹뷰
- 앱스토어 규약 위반 가능성, 네이티브 기능을 탑재해야 함
- ex. 푸시, 카메라, 네이티브 인증(페이스 타임, 지문 인식 등)
- 네이티브 + 멀티 웹뷰
- 웹뷰간 데이터 교환 방법을 초기부터 고안해야 함
- 앱에 저장소를 만들고 웹뷰에게 인터페이스를 제공하라
- ex. 결제, 주문 상세, 주문 목록
- 네이티브 + RN
- 변화가 많은 지면은 RN으로 개발, 그 외의 지면은 네이티브 개발
- 역량있는 네이티브 개발자가 필요하다.
- 네이티브 컨테이너 + 단일 웹뷰
- 앱 패키징 아키텍처와 무관한 고려 사항
- 네비게이션 룰을 확립하라
- 특정 화면의 직접 랜딩을 위한 앱스킴 디자인
- 사용자가 다이렉트로 접근할 수 있는 경로가 반드시 필요하다(deeplink)
- 향후 웹 서비스 만들꺼야 → universial link를 deeplink의 기본 스펙으로
- 공개용 웹뷰와 내부용 웹뷰를 분리하라
- 보안을 고려하라
- API 연동 토큰 및 앱 메타 정보 등 서버가 필요로 하는 정보를 어떻게 관리할지 고려하라
- 개발 환경을 구축하라
- 웹뷰와 데스크탑 브라우저는 다르다.
- 같은 기기 내 웹뷰, 브라우저 등도 차이점이 존재한다.
- 시뮬레이터와 실기기에서 작동하는 것도 차이가 존재한다.
- 각각에 대해 개발자가 경험할 수 있는 환경을 미리 준비하고 개발자간 쉽게 방법을 공유할 수 있도록 문서화하고 변경사항을 업데이트한다.
- 런타임 오류를 수집하라
- 디바이스 환경에 브라우저 환경보다 훨씬 더 다양하다
- AOS >>> iOS
- 캐싱을 적극적으로 활용하라
- 캐싱된 데이터와 서버 패치 데이터의 자연스러운 전환을 고려한 UX를 설계하라
- 네트워크가 안 될 때 사용자에게 보여줄 수 있는 데이터를 캐싱하라.
- 웹뷰의 라이프사이클을 인지할 수 있는 인터페이스를 설계하라
- 2번 탭 - 웹뷰, 3번 탭 - 네이티브
- 2번 탭 → 3번 탭 → 2번 탭 : 최신 정보를 패치하려면?
- 프리젠테이션 컴포넌트를 독립적으로 운영하라
- 접근성을 언제나 고려하라
- 네비게이션 룰을 확립하라
신규 개발 관점에서의 리스크 관리
- Communication
- 프로토타입
- API가 없어도, 디자인이 없어도 프로토타입은 가능
- 돌아가는 걸 보여주어라. 그래야 더 설득력이 있고 명확하다.
- 프로토타입은 PoC와 다르다.
- Mock API
- 미리 준비해서 일정을 최대한 맞출 것
- 어짜피 예측하지 못한 변수들은 반드시 생긴다.
유지, 개선 관점에서의 리스크 관리
- 레거시 코드를 존중하자.
- 모든 코드는 릴리즈 되는 순간 레거시 코드다.
- 서비스를 생존시킨 레거시 코드는 존중받아 마땅하다.
- 비난은 아무런 이익을 만들어 내지 못한다.
- 시각화되지 않은 문제는 불만일 뿐이다.
- 레거시 코드는 왜 분석하기가 힘든가?
- 기술적인 난이도가 높다.
- 역략 한계를 인정하고 공개한 후 함께 해결책을 찾아라.
- 잘못된 구조로 규모가 커진 코드
- 기술적인 난이도가 높다.
안정적인 프로덕트를 위한 코드 구조와 관리
- 유형 → 뒤섞이면 문제가 발생한다.
- 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에서 불러오게 하는 전략