한 종목에 투자금을 집중했다가 큰 돈을 잃어본 경험이 있나요?
- 주식, 채권 등의 다양한 자산을 종합적으로 고려한 포트폴리오 생성 시뮬레이터
- 과거를 분석하고 미래를 예측하여 손실 위험을 최소화하고, 위기에서 반등의 기회를 제공
- 본 시뮬레이터는 아래 질문에 대한 해답을 제공할 두 서비스로 구성
- 자산배분 : 현재 보유한 이 종목들의 비중을 어떻게 가져갔어야 지금 수익률이 최대였을까?
- 트레이딩 : 종목 상관없이, 내가 구상한 매매 전략으로 진행하면 일년 후에는 어떻게 될까?
- 시뮬레이션 생성을 위한 정보를 form에 입력하여 제출하면, 연동된 AI model이 분석결과 생성
- 진행한 시뮬레이션 히스토리 내역을 확인하여 실제 투자에 참고 반영
자산배분, 트레이딩 시뮬레이터 서비스 전반의 front-end 개발
- react-router-dom lib 사용 (Browser Router)
- page component들은 React.lazy를 통해 코드 스플리팅. 비동기적으로 로딩 (for 렌더링 최적화)
- asset allocation, trading 두 서비스로 묶이는 페이지들은 각각 sub route 생성
- 로그인이 필요한 페이지의 경우 login check를 담당하는 PrivateRoute를 만들어 감쌌음
- 해당 페이지들 상단에 login check code를 각각 포함시켰는데, 유지보수를 위해 분리
- google 로그인 사용
- login 버튼 클릭시 구글 로그인 페이지로 이동
- url parameter로 redirect uri 를 전달 ( ~/loginprocess )
- 구글 로그인 성공시 해당 redirect uri에 get method로 user token 생성 전달
- LoginProcess component에서 해당 token parsing
- token을 포함한 user 정보를 redux store에 저장
- isAuthenticated 등 인증 변수 활성화
- 로그아웃 시 user 관련 정보 및 변수 초기화, 메인 화면으로 이동
- react-hook-form, material-ui lib 사용
- 각 type별 input component 는 mui 사용
- 페이지 최상단 component에서 useForm Hooks 사용
- mui component를 Controller로 감싸고, control 함수를 각 input component Controller에 전달
- submit 클릭 시 form state를 가공하여 시뮬레이션 생성 axios 요청
const { handleSubmit, control } = useForm();
import TextField from "@mui/material/TextField";
import Slider from "@mui/material/Slider";
const onSubmit = (data) => {
// form data가 param으로 전달. 데이터 가공 후 시뮬레이션 생성 axios 요청
}
<form onSubmit={handleSubmit(onSubmit)}>
<Controller control={control} render={()=><TextField type="number" .../>} />
<Controller control={control} render={()=><Slider .../>} />
</form>
- redux
- account : userId, isAuthenticated, jwt 등
- simulation : isSimulating, simulationList 등
- 그 외 여러 page, component 공용 state
- useState
- chart mode, modal 등 지역 state 관리
- 종목 리스트 조회, 시뮬레이션 생성 등의 api 요청/응답 관리
- 공통 axios instance를 만들어 사용
- interceptors middleware를 추가하여 요청/응답 전후 에러 처리
const instance = axios.create({
baseURL : API_BASE_URL,
headers : { "Content-Type" : "application/json" },
withCredentials : false
});
instance.interceptors.request.use( // response도 동일한 방식으로 추가
(config) => config,
(error) => Promise.reject(error)
);
- 사용자가 생성한 시뮬레이션 결과를 직관적으로 전달하도록 적절한 chart 활용
- Chart.js 라이브러리 사용 ( Pie, Doughnut, Line, Stacked Line )
- Chart comonent props : labels(각 dataset의 이름) / datasets(chart에 표시될 수치들)
interface ChartProps {
labels : String[]
datasets : Array<{data: Number[]}>
// datasets.length === labels.length
}
- chart
- 사용자의 목적에 맞는 분석 도모
- line chart : 선형, 로그형 toggle 추가
- pie chart : list view toggle 추가
- chart zoom + panning + reset 기능
- CSS
- styled-componenet
- 기존에는 bootstrap과 className을 혼용하여 수정이 번거로웠음
- styled-component로 통일하여 design 파일 분리 → 유지보수 용이
- 반응형 작업
- window 크기에 따른 component 크기 자동 조정
- @media 쿼리와 react-responsive lib 활용
- mui
- Text, Date, Number, Range, Slider 등의 요소를 활용
- form control 직관화. 모바일 호환
- styled-componenet
- modal
- 브라우저
- chrome secret mode + without cookie
- 도구
- Lighthouse (3회 측정 평균)
- 대상
- 메인페이지 ( ~.co.kr/ )
- 시뮬레이션 생성 페이지 ( ~.co.kr/asset/backtest )
- 시뮬레이션 결과 확인 페이지 ( ~.co.kr/asset/result )
- 지표
- 성능 종합 점수
- FCP (First Contentful Paint) - 첫 text/image 표시 시간
- LCP (Largest Contentful Paint) - 최대 text/image 표시 시간
- T2I (Time to Interactive) - 완전히 페이지와 상호작용할 수 있게 될 때까지 걸리는 시간
- TBT (Total Blocking Time) - FCP와 T2I 사이 모든 시간의 합
- CLS (Cumulative Layout Shift) - 표시 영역 안에 보이는 요소의 이동 측정. 시각적 안정성
- Speed Index - 페이지 콘텐츠가 얼마나 빨리 표시되는가
-
React.memo 적용
-
컴포넌트 리렌더링 상황
- props 변경, state 변경, 부모 컴포넌트의 리렌더링, forceUpdate 함수 실행 ( 발생빈도 ↓ )
-
필요성
- 컴포넌트의 props, state가 변경되면 rerendering 되어야 한다.
- 하지만 부모 컴포넌트가 리렌더링 되었다 하여 무조건 자식 컴포넌트를 리렌더링 하는 것은 비효율적
- 자식 컴포넌트의 props, state가 변경되지 않았다면, 기존 컴포넌트 재사용 하도록 React.memo 사용
-
대상
- props를 전달받지 않는 단순 컴포넌트
- props가 자주 변하는 컴포넌트는 지양 → prevProps, nextProps 비교에 메모리 낭비
- 각 라우터 최상위 index.tsx 파일은 적용 X
- 하위 component에 적용
// before export default Component; // after export default React.memo(Component);
-
-
useMemo, useCallback 적용
-
목적
- 컴포넌트를 리렌더링할 때, 변경되지 않은 내부 함수/값을 새로 계산하지 않고 재사용
- 자식 컴포넌트가 React.memo로 최적화되어 있는 경우, 자식에게 props로 전달하는 값, 콜백함수 등을 useMemo, useCallback으로 생성하여 전달.
-
차이점
- useMemo : 주로 숫자, 문자열, 객체 등 일반 값 기억
- useCallback : 주로 함수
- 둘 다 값/함수를 저장하는데 사용할 수 있다.
- 아래 두 코드는 동일한 결과를 불러온다.
useCallback(()=>{ console.log("hello world"); }, []); useMemo(()=>{ const fn = () => console.log("hello world"); return fn }, []);
-
사용하지 말아야 할 경우
- host component(
div
,span
,a
,img
등)에 전달하는 모든 항목 (리액트는 신경 안씀) - leaf component (최하위 자식 컴포넌트)
- 전달하려는 값/함수가 새로운 참조여도 상관 없는 경우
- host component(
-
사용해야 하는 경우
- 하위 트리에 많은 consumer가 있는 값을 전달하는 경우
- 계산 비용이 많이 들고, 사용자 입력 값이
map
,filter
사용했을 때와 같이 이후 렌더링 이후로도 참조적으로 동일할 가능성이 높은 경우 - 자식 컴포넌트에서
useEffect
가 반복적으로 trigger 되는 것을 막고 싶을 때
-
-
useState 함수형 업데이트로 변환
-
기존
-
setFoo 함수에 param으로 변수 직접 전달
→ useCallback 최적화시 해당 변수를 의존성에 넣어주어야 하며, 값이 변할때마다 재할당됨 (사실상 Hooks 최적화 의미 없음)
-
-
이후
-
setFoo 함수에 param 조작 방식을 함수로서 전달
→ 의존성이 필요 없어 재할당되지 않아 메모리를 아낄 수 있음. 최적화에 적합
-
하지만 본 프로젝트에서는 redux로 상태관리했고, useState를 사용한 경우는 loading변수 정도라 크게 의미가 있지는 않을 것으로 예상
// state 값을 직접 사용. param 직접 대입 (set 함수 내에 params 전달) const onIncrease = useCallback(()=>setNumber(number+1), [number]); // 이전 상태값을 어떻게 업데이트할지 정의하는 방식 -> 감시 배열 비어도 됨 // set 함수 내에 **함수** 전달 const onIncrease = useCallback(()=>setNumber(prev=>prev+1), []);
-
-
-
코드 스플리팅 : React.lazy, Suspense 적용
-
대상
-
현재 페이지에 포함되어 있지만, 사용자가 클릭하기 전이라 보여지지 않는 컴포넌트들
ex) Modal, Route 등
-
본 프로젝트에서는 두가지 유형으로 적용하였다.
-
리액트 공식 문서 추천 : Routes 내부
→ 비로그인 상태 최초 화면 로딩 속도가 눈에 띄게 빨라지는 것을 느꼈음
-
Modal (image, result)
-
-
-
적용 예시
const Home = lazy(() => import('./routes/Home')); const About = lazy(() => import('./routes/About')); const App = () => ( <Router> <Suspense fallback={<div>Loading...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </Suspense> </Router> );
-
-
압축 적용
- compress-create-react-app lib 적용
- build : “react-scripts build && compress-cra”
메인페이지 ( ~.co.kr/ )
시뮬레이션 생성 페이지 ( ~.co.kr/asset/backtest )
시뮬레이션 결과 확인 페이지 ( ~.co.kr/asset/result )
-
chart zoom
- 현상
- chart-plugin-zoom lib 설치 후 zoom, panning 옵션 추가
- zoom, panning 기능이 정상 작동하였으나 y축으로만 반응하고 x축 방향은 반응 없음
- 원인
- API 응답 데이터 내 x축 형식이 datetime 형식이어서 발생하는 오류로 파악
- chart.js ㅇ에서 해당 형식 값의 날짜 전후 비교를 못하는 것으로 추정
- 해결
- x축의 type을 datetime time으로 변환 처리하여 해결
- 현상
-
log mode chart
- 실제 서비스로 배포되지 못하였다.
- 프로젝트 담당자 8명 중 4명이 전문연구요원
- 해당 인원들의 병역 문제로 프로젝트 이탈 (자세한 내용은 사내 보안상 생략합니다.)
- 본 프로젝트는 AI model 이 핵심인데, 모두 전문연구요원 담당이었음
- 이런 상황에 회장님이 원하는 서비스 노선이 변경되어 본 프로젝트는 여기서 마무리 되었음
- 최적화 각 단계별 향상 성능을 측정하지 않았다.
- 각 단계별 개선 성능이 측정 환경에 따라 발생하는 오차에 가려질 것이라 생각하였다.
- 최적화가 끝나고 보니 어떤 단계가 제일 dramatic 했는지 궁금하긴 하다.
- 하지만 이를 위해서 다시 원상태로 돌려서 다시 진행하기에는 그 과정이 너무 힘들었기 때문에 그러지 않았다.
- 다음에는 우선 순위를 정해보고 세부 단계별로 측정해볼 것이다.