반응형

최근 취직한 직장에서 실시간 모니터링 툴을 만들고 있다. 

결론부터 말하자면 월부터 3월까지 일하면서

(실시간 데이터 업데이트시) 렌더링 시간을 700~600ms에서 390~290ms로 대략 100% 개선하였다.

 

이때 사용한 기법을 간단하게 적어보고자 한다.

 

비슷한 모니터링 툴 예시 - grafana (https://grafana.com/)

실제 제품과 코드를 보여줄순 없고 비슷한 제품으로는 그라파나(https://grafana.com/) 라는 제품이 있는데 거의 80%쯤 비슷해서..? 이 제품을 보여주고 예시로 설명하고자 한다.

react-grid-layout(https://github.com/react-grid-layout/react-grid-layout)이라고 레이아웃 배치때 사용한 라이브러리도 똑같다.

 

배경 지식

 

어떻게 개선했나 리펙토링을 설명하기에 앞서

어떤 구조로 되어있나를 간략하게 설명하고자 한다.

 

화면에는 두가지 api 정보가 있다.

 

1. layout 정보 (빨간색) : 전체 레이아웃 정보와 레이아웃과 관련된 차트 정보를 담고 있다.

레이아웃 좌표(x, y축), 차트 종류(area, grid, bar 등) 차트를 각각 '어디'에 배치할지 정보를 담고 있다.

 

2. chart 정보(파란색) : 차트 내부의 실제 차트 정보를 api를 통해 불러온다.

 

프로젝트는 CRA로 구현되었는데 해당 화면을 들어가게 되면 1차적으로 layout정보를 api를 통해 불러오고,

레이아웃을 불러오면 2차적으로 각각 차트 데이터를 api를 통해 불러오게 되는 구조다.

 

문제 원인

렌더링에 걸리는 시간 - 대략 600~700ms

react profiler를 돌려본결과 느린 이유를 크게 아래의 3가지로 압축가능한듯하다.

설명하고 추후 해결방안을 제시할 예정

 

1. presentational and container pattern 이용

 

전체 페이지가 presentational and container pattern으로 구현되어 있고 로직이 최상위에 몰려있었다.

 

최상층에서 layout 정보를 받아 상태로 가지고 있고 (mobx + useState 혼합) 부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 자동적으로 전부 리렌더링되는데 하나만 바뀌어도 화면 전체 리렌더링을 유발했다. 

전임자가 Memo등을 사용해서 최적화를 시도한듯한데 신통치는 않았던듯..

 

끔찍한 가독성은 덤이고 이 구조는 재앙(?)을 불러오게 된다. 2번에서 후술

 

2. derived State 사용

class EmailInput extends Component {
  state = { email: this.props.email };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = event => {
    this.setState({ email: event.target.value });
  };

  componentWillReceiveProps(nextProps) {
    // This will erase any local state updates!
    // Do not do this.
    this.setState({ email: nextProps.email });
  }
}

 

코드 예시 - 리엑트 공식 링크 참고 (https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html)

 

derivedState란 말 그대로 상태로부터 파생되는 State이다.

위 코드 예시처럼 prop으로 어떤 상태를 내려받아 전처리나 필요한 데이터만 따로 빼내어 상태로 관리하는 형태이다.

 

기존 코드에서는 presentational-container 패턴으로 layout api 정보를 최상층에서 prop으로 주입하고,

차트 내부에서 derived state를 사용해서 chart api 데이터와 혼합해 관리했다.

 

아까 캡처 사진을 보면서 다시 이해해보자.

layout api state(빨간색)에는 모든 레이아웃의 정보가 담겨 있다.

chart 레이아웃 정보는 array 형식으로 담겨 있는 형태였는데

chart 내부에서 layout의 정보를 사용하려고 chart api state(파란색)과 혼합해 전처리하고

derivedState를 만들게 된다.

 

위 렌더링 profiler를 보면 Run MicroTask란 작업이 엄청 많은것을 볼 수 있는데

어떤 상태가 바뀌면 derivedState도 재생생하면서 rerendering을 무수히 많이 발생시켜 렌더링 속도가 매우 느려지게 된다.

예를 들어, 파란색 chart를 오른쪽으로 약간 이동시키기만 해도 최상층부터 맨 아래까지 수많은 렌더링이 발생하는것...

 

처음엔 Run MicroTasks라길래 네트워크 중복 호출인줄 알았는데 아니였고

수많은 useEffect와 derivedState 문제인것으로 추측된다.

 

3. api 호출때 Batching 처리 X 

 

각 레이아웃에서 chartData를 api로 호출할때 한번에 묶어 호출하는게 아니라

차트 하나하나가 api콜을 해서 렌더링시키게 된다.

 

30개의 차트면 30개의 api 콜을 하는 방식인데

한두개가 네트워크 지연으로 느리게 오게 되면 다시 리렌더링을 발생시키게 된다.

 

해결 방안

리펙토링에서는 위 3가지 문제점을 제거하는 방안으로 일단 리펙토링을 진행했다.

600ms -> 300ms로 100% 빨라진 모습

 

그래서 이후 점진적인 리펙토링이 필요함에도 일단 상당히 빨라진것을 볼 수 있다.

 

1. presentational and container pattern 이용 -> Hook + 전역 State 방식으로 변경 

 

전역 state를 사용하면 변경된 상태들과 관련된 react component들만 리렌더링 시킬 수 있다.

1번은 사실 pattern 문제라기보다는 가독성 + 2번 문제 해결을 위해 리펙토링했다.

 

2. derived State 제거

 

정확히는 불필요한 rerendering을 줄이기 위해서 UseEffect를 제거했고,

UseEffect가 필요한 derived state를 제거했다. 

 

derived State를 사용한 이유를 보니

복잡한 3~4 depth의 api를 전처리해 chart 내부에서 사용하려는 이유도 크길래 

 

'전처리'해야하는 상태를 normalizr로 따로 분리하여 1 depth로 전처리후 전역 state로 관리하고

chart에 각각 unique한 id를 발급하여 전역 state를 id로 접근할 수 있게 리펙토링했다.

 

setState({
    locateX : props.layoutData.properties.x;
    locateY : props.layoutData.properties.y;
    threshold: props.layoutData.properties.y;
    chartData : chartData.data.chartData;
    chartAxisX : chartData.data.axisX,
    chartAxisY : chartData.data.axisY,
	....
})

이렇게 사용하던 코드를 (실제 코드와는 다른 예시 코드임)

const {locateX, locateY, threshold, chartData, chartAxis, chartAxisY } = useLayoutData(props.chartId);

normalizr를 사용해 위와 같이 리펙토링했다.

 

3. Batching처리 로직 추가 

 

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];

Promise.allSettled(promises).
  then((results) => results.forEach((result) => console.log(result.status)));

// Expected output:
// "fulfilled"
// "rejected"

promise.allSettled()를 사용하면 위와 같이 여러개의 api콜이 다 올때까지 await를 걸 수 있고, 에러처리도 해준다.

 

각 차트에서 api 콜을 하는게 아니라, 배칭처리를 통해 모든 api콜이 올때까지 기다린후

한번에 chart들을 업데이트해주는 방식으로 변경했다.

 

해당 방식만을 사용하면 chart가 하나 추가되었을때 다시 모든 api 콜을 하는 단점이 있었는데

이것은 requestTime을 기록해서

 

현재시간-refreshTime >= refresh로 

 

필요한 데이터만 불러오도록 예외처리시켰다. 

 

다음 분기에는 모든 api를 하나로 통합하고 마치 GraphQL처럼(혹은 graphQL로)

프론트에서 질의문을 보내 필요한 데이터만 받아오는 형식으로 변경하지 싶은데 일단은 배칭처리로 묶는거까지 마무리했다.

 

여담으로 이 페이지를 만든 전임자가 내가 입사하기 전에 나간 상태여서 

걍 맨땅에 헤딩하는식으로 했기 때문에 코드 이해에 애좀 먹었다;;

 

지금 생각해도 이걸 신입이 어떻게 했지? 싶은데

못했으면 짤렸으려나? 

반응형
반응형

지난 2022년 12월 ~ 2023년 2월 1일간 했던 일을 간단하게 메모해두려고 한다

아는사람?의 추천을 받고 작년 10월에 새로운 회사에 입사했다.
입사하고 기존 레거시 프로젝트의 react 개발자로 일하게 되었는데,
코드를 받아보고 정말 충격을 받았었다.

가만히 놔두면 렌더링이 무한 발생하는 실시간 차트 페이지..; 이 페이지는 결국 버리고 새로 만들었다

정말 개판으로 짜였다고 느껴진 스파게티 코드와
!important와 inline으로 갈겨놓은 css들을 보고 경악을 했어서 처음엔 나갈려다가
채용 한파에 1년만 버티자 하고 울며 겨자먹기로 ㅠㅠ 일하게 되었는데

하다보니 애증?이 생겨서 열심히 하고 있는 중이다.

1. ground rule 다시 만들기

(아래 링크는 입사 이후 회의한 내용)
https://www.notion.so/10-13-e2521a2a8b5944ddb4e873da3f00cd26

 

10/13 회의 내용

1. import alias - 반영 완료

www.notion.so


프론트 개발은 중간에 한분이 나가시고 사실상 사수분 한명이 다 하고 있던 상태였는데 (어떻게 하셨는지 지금도 의문..)
협업을 위해 간단한 ground rule을 다시 세우고 작업하기로 했다.

프로젝트가 몇년간 여러 사람을 거쳐가면서 누구는 presentational-container pattern을 사용하고 누구는 그걸 따라하겠다고 굳이 mobx에서 전역 데이터를 꺼내서 최상단에서 최하단 7층까지 내려보내고 있고;; 통일성이 필요했다.

2. 디자인 시스템 - 공통 컴포넌트 분리하기

디자인 시스템 예시(출처 : https://coyleandrew.medium.com/a-quick-guide-to-creating-a-design-system-7888e267171f)

기존에는 같은 컴포넌트를 ctrl c + ctrl v 해 사용하거나, material UI를 그대로 박아넣어서
수정 사항이 생겼으면 모든 컴포넌트를 바꾸는 노가다를 해야했다.
(공통 컴포넌트가 있긴 했지만 사람이 계속 바뀌면서 쓰이지 않았다.. 이유는 3번 storybook 문단에서 후술함)

실제로 입사 초기에 dialog에 어떤 기능을 추가하기 위해서 46개의 dialog가 포함된 파일을 수정했었다. ....

그래서 디자인팀에 요청해서 재사용성을 높이기 위해서 같은 컴포넌트를 묶어주고
컴포넌트화 시키는 작업을 하자고 제안했는데, 마침 디자인팀도 일관된 사용자 경험을 위해서 비슷한 고민을 하고 있던 참이라서 공통 컴포넌트 작업을 시작하게 되었다.

디자인 템플릿은 구글의 Material UI를 참고했고, 코드 형식은 Headless UIcompound Pattern를 참고했다.

공통 컴포넌트인 accordion 예시

1. 우선 많이 쓰이는 컴포넌트 (Dropdown, button 등)등을 분리했고,
2. (button 같이 여러 style 바리에이션이 있는 경우) 디자인에 따라서 primary, secondary 등 여러 css 템플릿을 만들었다. props로 전달해서 css 형식을 정하게 된다.
3. 그리고 공통 컴포넌트 내부에서 상태를 Depth에 상관없이 공유하기 위해서 Compound pattern과 Context API를 사용해
작성했다.

 

기능과 스타일의 분리

 

구현시 재사용성을 위해서 기능(logic) 부분과 style이 분리되게 설계했다.

한 기능(ex- dropdown)에 여러 style을 갈아끼울 수 있으며,

기능과 style이 서로 독립적이니 객체지향의 OCP처럼 점점 필요한 기능을 확장해나갈수 있게 설계했다.

 

위 예시 사진인 accordion도 dropdown의 logic 부분 코드을 재활용하고 style을 따로 입힌 예시이다.

 

참고) compound pattern + context api - https://leon-dunamu.github.io/2021/07/21/react-compound-component/

2-1) 라이브러리 모듈화

이때 라이브러리를 사용했다면(ex - table 이나 Map API) 반드시 공통 컴포넌트로 한번 감싸고
외부에 인터페이스를 공유하는 식으로 작성했다.

왜냐하면 이 공통 컴포넌트를 사용하는 입장에서는 내부 구현을 알 필요가 없다.

지도로 비유하자면

컴포넌트를 사용하는 입장에서 Kakao Map인지 Naver Map인지는 관심이 없고 '지도 자체'만 필요하기 떄문이다.

또한, 모듈화가 이뤄져야 만약 라이브러리에 문제가 생겼거나 교체가 필요하다면 인터페이스는 놔두고 공통 컴포넌트 내부만 바꾸는 식으로 빠르게 교체할 수 있다.

+ 모듈화를 통해 '경계'를 만들고 라이브러리 전용 테스트를 작성해서 만약 라이브러리 버전업시 문제가 발생하면 빠르게 캐치할수 있도록 jest로 test code를 작성했다.

추가적으로 기본적인 dropdown 컴포넌트 등을 만들때엔 기존에는 mui4를 사용했는데 점차 deprecated 시킬 예정이라서 외부 라이브러리를 사용하지 않고 직접 작성했다.
(지금 생각해보니 아이콘은 mui자너..?)

3. Storybook 도입

https://storybook.js.org/

사람이 자주 바뀌는데 문서화가 안되있으니
전임자들 코드를 분석해보니 비슷한 코드를 2~3번씩 작성하거나
1회용이라 생각해서 그냥 작성해 때려박는 문제점이 있길래 인수인계 + 공통 컴포넌트 테스트 용으로 storybook을 도입했다.

1. 공통 컴포넌트를 작업한 뒤

2. storybook을 작성하고

3. 그 storybook을 jest에서 import해서 테스트를 돌린다 (mocking - MSW)
이러면 테스트 대상이 어떻게 렌더링되나 실제로 브라우저에서 볼 수 있는 장점이 있다.

테스트 방식은 전에 글을 한번 썼었다.
https://lodado.tistory.com/73

4. visual regression test 용으로 chromatic 에 연동해 확인한다.

(그런데 모든 snapshot을 체크해보기에는 사람이 부족하기도 하고 chromatic은 단순 참고 용도로 사용하고 있다..
확인하는 부분은 나중 QA팀에 요청할 생각이다)

4. 데이터 정규화 - normalizr 도입

(normalizr 소개는 quick start 참고 - https://github.com/paularmstrong/normalizr/blob/master/docs/quickstart.md)

{
  "id": "123",
  "author": {
    "id": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": [
    {
      "id": "324",
      "commenter": {
        "id": "2",
        "name": "Nicole"
      }
    }
  ]
}


normalizr를 간단하게 설명하자면 위와 같이 깊은 depth를 가진 JSON 파일에서 data.comments.commenter.id 만 3으로 수정한다고 생각해보자. 매우매우매우 끔찍하다.
그래서 정규화 과정을 통해 데이터를 id를 통해 참조하는 쓰기 좋은 포맷으로 바꿔준다.

{
  result: "123",
  entities: {
    "articles": {
      "123": {
        id: "123",
        author: "1",
        title: "My awesome blog post",
        comments: [ "324" ]
      }
    },
    "users": {
      "1": { "id": "1", "name": "Paul" },
      "2": { "id": "2", "name": "Nicole" }
    },
    "comments": {
      "324": { id: "324", "commenter": "2" }
    }
  }
}


위와 같이 id를 통해 참조 가능한 1 depth로 줄여주는 좋은 라이브러리다.

내가 맡은 파트는 비유하자면
"레이아웃에 drag and drop으로 자유롭게 배치 가능한 실시간 주식 차트"인데, API 구조가 상당히 복잡하다.

주식 차트들을 보기 위해서 다음과 같은 과정을 거친다.

1. 유저의 LayoutList 목록을 API 콜 A로 가져온다.
2. 유저가 LayoutList 중 하나의 Layout을 선택한다.
3. 해당 선택된 Layout에는 Chart들의 정보를 담는 ChartList가 있다. ChartList 에 있는 chart 들의 key들을 API 콜 B로 보내서 chart를 그릴 데이터를 API로 받아온다.

기존 코드는 해당 API를 사용하기 위해서 매우 복잡하고 파악이 힘든 전처리 과정을 거치고 있었는데

normalizr를 사용해 id 값만 있다면 어느 데이터나 (전역 state를 통해) 접속하고 , 수정 가능하게 리펙토링하니
가독성이 높아지고 유지보수성도 확실히 향상되었다.

나도 써보기 전까진 긴가민가 했는데 이젠 안쓰고는 못살꺼 같다.
API 구조가 복잡하면 꼭 써보자.

 

2023-02-18 추가 //

비슷한 내용 찾아서 추가..

 

https://www.youtube.com/watch?v=HYgKBvLr49c 

 

 

5. Repository 패턴 도입

안드로이드의 레포지토리 패턴 예제


프론트에서 필요한 데이터의 접근과 비즈니스 로직을 분리하기 위해서 API 접근에서 레포지토리 패턴을 추가했다.
추가적으로 필요한 데이터 전처리, 데이터 정규화(normalizr), 데이터 캐싱 처리를 레포지토리에서 관리하도록 layer를 분리했다.

기존에는

component <-> (state library(mobx, redux 등등..)) <->  API Call

위와 같은 구조로 되어 있었는데 필요한 데이터를 뽑아내는 전처리 로직과 비즈니스 로직이 뭉쳐있어서
API 수정이나 프론트 코드 수정에 유연하지 못했다.

repository를 추가한다면

component <-> (state library(mobx, redux 등등..)) <-> repository  <->  API Call

위와 같은 레이어를 거치는셈이다.
repository가 데이터 관리를 수행하고 사용하는 입장에서는 그냥 데이터 요청만 해서 써도 된다.

여담으로 repository가 데이터 전처리까지 해도 좋은가(=프론트에서 전처리해도 좋은가) 조사해본 결과 데이터 전처리는 가능하면 백엔드에서 하는게 좋은 편인거 같다. 프론트엔드는 싱글 쓰레드로 상태관리와 렌더링하는거만 해도 바쁘다.
그래서 일부 기업은 프론트가 백엔드인 API gateway server까지 전담하는 경우도 많다고 한다.

우리팀도 점진적으로 리펙토링 해가며 전처리 로직을 백엔드로 넘길 예정이다. graphql을 도입하여 프론트에서는 질의문만 백엔드에 날릴까 검토중인데 큰 작업이라 진짜 할지는 모르겠다.

추가적으로 백엔드 API가 기존에는 페이지 별로 있었는데 컴포넌트 단위로 통합시킬 예정이다. 그래서 1차로 동일한 인터페이스를 가진 레포지토리로 api를 통합하고 2차로 레포지토리를 바꾸는 식으로 점진적으로 api를 통합해나갈 계획이다.


하다보니 느낀점이
내가 아직 신입인데 올바른 방향으로 가고 있나 걱정되기도 하고 이상한 코드는 이상하게 짜인 이유가 있었고 다시 만들어도 비슷한 레일을 따라가는거 같긴 하다..?
혹시 예상치 못한 버그가 생길까봐 조심스레 개선하고 있는 중이다.

그래도 컨택스트 파악 안된 코드를 다음 사람에게 물려주고 싶진 않아서 노력하고 있다..🔥

반응형
반응형

프론트엔드 코드의 특성

  1. 코드의 잦은 변경(디자인)
  2. 짧은 생명주기
  3. 정량적 평가(디자인, UI/UX)의 어려움

이는 테스트의 어려움으로 이어지게 된다.
프론트엔드의 특성을 잘 살린

  1. 잦은 변경에 대응 가능한 (react-testing library)
  2. 생명 주기에 구애받지 않는 (모듈화후 통째로 교체)
  3. 시각적 요소 평가가 편하게 (storybook & chromatic)

코드, 테스트 코드를 작성하는게 좋다.

 

프론트엔드 테스트의 종류

크게 두가지 요소로 나눌 수 있다.

  1. 기능적 테스트(소프트웨어 공학적 테스트)
  2. 시각적 테스트(디자인, UI/UX 등)

 

기능적 테스트

Static Test
정적 테스트는 코드를 실행시키지 않고 테스트를 하는 것
오타나 type에러 같은 개발자의 실수로 인해 발생하는 에러를 미연에 방지
대표적 도구 : typescript, eslint, prettier

 

unit
각 모듈을 단독 실행 환경에서 독립적으로 테스트
특정 컴포넌트를 렌더링해서 깨지지 않는지 확인하는 것을 예

Button, Dialog같은 작은 단위

 

intergration
통합 테스트는 두 개 이상의 모듈이 실제로 연결된 상태를 테스트

UI와 API 간의 상호작용이 올바르게 일어나는지
state에 따른 UI의 변경이 올바르게 동작하는지

GNU, 하나의 페이지 같은 큰 단위

 

e2e
실제 사용자의 입장 및 환경에서 테스트하는 것

 

위로 갈수록 테스트 비용이 비싸지고 느려진다!

권장 테스트 비율은

unit 70%
intergration 20%
e2e 10%

 

프론트엔드 관점의 테스트

 

visual test(Visual Regression Test)

 

시각적으로 무엇이 달라졌나 테스트
정량적 요소(디자인 등)로 평가하기 어렵기 때문에 자동화가 어렵다

 

대표적 도구 : storybook, chromatic

 

chromatic - Visual Regression Test

 

chromatic을 사용하면 화면의 bitmap 단위로 비교가 가능하다
원본(snapshot)과 변경 코드가 시각적으로 무엇이 달라졌는지 분석

 

테스트 환경

 

브라우저 환경

 

장점 : 크로스 브라우징 & 기기 호환성 테스트 가능
단점 : 느리다
ex) cypress, playwright

 

Node.js 환경


장점: 빠르다
단점: 크로스 브라우징 & 기기 호환성 테스트 가능 불가
ex) jest

 

프론트엔드 테스트 대상

(시각적 요소, 사용자 이벤트 처리, API 통신)
테스트의 대상은 다음과 같이 크게 3가지

 

1. 시각적 요소

시각적 요소 - Visual Regresstion testing(chromatic 참고)

컴포넌트가 깨졌는지, side Effect가 없는지 확인 용도로 쓰고
왜 깨졌는지는 기능적 테스트에서 확인하는게 빠르다.

 

2. 사용자 이벤트 처리

프론트엔드 대부분의 로직은 다음과 같은 로직을 가진다.

  1. 1. Event가 발생한다 (click, enter event 등)
  2. 2. Handler가 event를 처리하고 Effect를 발생시킨다. <-
  3. 3. Effect는 View를 변경시킨다.

테스트에서 중점으로 처리해야 할것은 2번


이벤트가 발생하여 어떠한 효과(effect)를 가져오는가를 테스트

view, style보다는 비즈니스 로직에 중점을 둔다.
시각적 요소는 storybook, Chromatic에게 위임

 

1번은 브라우저, react library에서 알아서 처리하니 믿음을 가져라
2번이 주 로직
3번은 2번이 제대로 처리되었다면 정상적으로 동작하게됨(의존성을 가짐)

 

하지마라

  1. 컴포넌트들이 제대로 합성되는가(view?)
  2. 컴포넌트들이 화면에 제대로 렌더링 되는가(view)
  3. 컴포넌트에 어떠한 동작을 했을 때, 무엇인가 트리거가 되는가(event<->handler 연결)

 

Testing library(jest)

 

Testing-library는 이름 그대로 UI를 사용자 관점에서 테스트할 수
있도록 도와주는 라이브러리

 

내부 상태 변경과 같은 상세한 구현(implementation details)을 테스트하는
기존의 프론트엔드 테스트의 문제점을 개선하기 위해 등장

사용자는 컴포넌트 내부의 상태가 어떻게 바뀌고,
컴포넌트 내부 메서드가 어떻게 호출되는지에는 전혀 관심이 없기 때문에,
테스트 코드를 작성할 때도 이에 대해선 테스트하지 말고
사용자 관점에서 테스트하자.

 

2, 3번을 잘 처리하면 테스트가 잘 되었다는 믿음
리펙토링을 해도 잘 동작하는지 확인 가능

 

3. API 통신

 

백엔드는 정상적으로 동작한다고 가정

주 목표는 API 통신으로 받아온 데이터를 넣으면 컴포넌트가
'정상적으로 동작 하는가' 중점

크게 2가지 방식

 

1. 테스트용 서버 or 라이브러리

테스트용 서버를 가지는건 공수가 많이 들기 때문에
"MSW"라는 라이브러리 이용

서비스워커가 API를 가로채고 가짜 데이터를 응답해준다.

 

2. api mocking

api 자체를 mocking
axios를 호출하면 데이터를 전해주는 가짜 함수로 변경

취향에 따라 사용

 

최근엔 MSW를 많이 쓰는 느낌
(storybook + jest + msw 연동해서 환경설정을 구축해놓으면
시각적 & 기능적 테스트에 한꺼번에 편리하게 사용가능)

 

실제 api 테스트를 작성하다보니 느낀점 (+ 2023.3월 추가)

 

보통 리엑트 컴포넌트 코드를 나누자면

 

1. style(UI, styled component) 

2. business logic(hook, 전역 state, api 등)

 

두가지로 세분화할 수 있는데

 

간단한 api + logic이면 그냥 storybook + jest + msw 연동시켜서 테스팅 시키는 방식으로 테스트 하면 편한거 같고

(이때도 대다수의 테스트 대상은 2번이다)

 

복잡한 인터렉션 + api는 위 방식으로 테스트하기 너무 복잡한거 같다. 

(실시간 모니터링 툴을 개발중이다보니 한 컴포넌트에 연동되는 api가 3~6개가 되는 케이스도 있었다;;)

2번만 따로 jest + mocking api로 business logic를 테스트 후  

style 부분에서 연동 & 호출이 잘되는지 연동 테스트 하는 방식이 좋은거 같다.

 

예를 들어 

 

const Example = ({onClick}) => {
  return <button type="button" onClick={onClick} /> 
}

위와 같은 컴포넌트는 style만 정의되어 있고 business logic(onClick)은 다른곳에서 주입받는다고 가정해보자. (hook이나 전역 state를 사용할수도 있고 방법은 여러가지)

 

onClick이라는 함수를 jest에서 jest.fn()로 mocking하고 

tobeCalled(), tobeCalledWith()등 호출을 확인하는 함수를 사용하면 

테스트 상황이 주어졌을때 실제로 business logic을 올바르게 호출하는지 확인 가능하다. 

 

너무 내부로직이 비대해서 쪼갠 케이스인데 이런 케이스는 잘 없지 싶다

 

잘하는법? (나도 몰라..)

 

테스트는 비용이다
-> 불필요한 테스트 최소화
-> 동어반복적 테스트 방지

 

부작용을 최소화하자

로직을 순수함수로 작성한다면 항상 input이 동일하다면

마지막 output이 동일할꺼라 예측 가능하다.

 

즉, 부작용(side Effect)가 없는 순수함수의 조합으로 코드를 작성하다면

테스트시 end-to-end로 테스트가 가능하니 테스트 작성 & 유지보수가 쉬워짐

 

다만 부작용이 없는 코드는 나오기 어려우므로

UI <-> business logic <-> data(접근) 이라고 나눠본다면

 

business logic를 순수함수로 작성하고 테스트하려고 노력하고,

UI, data 부분에 부작용 함수 부분을 넘겨주자..?

 

FIRST 법칙

F (Fast 빠르게)
I (Independat 독립적으로 - sideEffect 영향 X)
R (Repeatable 반복가능하게 - 결과가 매번 동일)
S (Self-Validating 자가검증하는 - 결과가 True or False)
T (Timely 적시에 - 필요할때)

 

 

코드(테스트 대상) 짤때 SOLID 법칙 응용

약간 다르긴 하지만 본질은 비슷하다

핵심 -

  1. 함수는 하나의 기능만
  2. 모듈화(ex - 라이브러리 교체시 인터페이스는 놔두고 통째로 교체)
  3. 컴포넌트 간의 의존성 제거
  4. 로직과 스타일의 분리 (container-presenter패턴, hook 등등)

 

Given, When, Then 패턴 사용

 

test('button click event', () => {
// Given
const data = $4
const spyOn = jest.fn();
render(<button type="button" onClick={spyOn}  />)

// When
const button = screen.getByRole('button');
button.click();

// Then
expect(spyOn).tohaveBeenCalled();
})

가독성 좋은 테스트는 기능 명세서 역할도 가능하다 

유저의 행동에 중점을 두고 작성

 

Given -> 주어진 환경

When -> Event Trigger

Then -> Effect Result

 

BDD에서 파생된 개념인거 같은데 깔끔하게 시나리오를 짤 수 있어서 좋은거 같다

 

라이브러리 테스트(optional)

 

필수는 아니지만 라이브러리 버전업(or 교체)시 무엇이 변경되었는지(or 무엇이 라이브러리 종속적인지) 

빠르게 알기 쉽도록 라이브러리 관련 코드에 테스트 작성

 

또한 라이브러리(ex-버튼, 테이블 라이브러리 등등)을 사용한다면 모듈화하고 테스트를 작성해놓으면 

나중 교체할때 어떤 기능이 부족하고 무엇이 문제인지 빠르게 인지 가능하다.

 

--추가 

일하다 yarn.lock이 꼬여서 라이브러리가 터지는 정신나가는 케이스를 봐서

필수는 아니지만 틈틈히 라이브러리 테스트를 작성해놓으면 좋을꺼같다..?

 

 

 

reference

 

1. 좋은 함수 만들기 - 부작용과 거리두기 (tistory.com)

https://velog.io/@teo/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-MV-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94
https://kooku0.github.io/blog/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90-solid-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0/
https://jbee.io/react/testing-1-react-testing/
https://blog.mathpresso.com/%EB%AA%A8%EB%8D%98-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%9E%B5-1%ED%8E%B8-841e87a613b2

반응형
반응형

공통 컴포넌트를 만들면서 서체 적용 작업(라고 쓰고 노가다)을 하고 있는데

위와 같이 부모 컴포넌트에 font-family를 적용해도 

하위 컴포넌트에는 font-family가 적용되지 않는 문제점이 있었다.

 

그 이유를 찾아본 결과

global.css에서 모든 컴포넌트에 default로 폰트를 먹이고 있었고, font-family가 상속이 되는게 아니라 

하위 컴포넌트에서는 위 default font가 적용이 되어.. 무시된 것이었다.

 

해결방법은 간단한데,

 

font family가 앞에서 뒤로 적용된다는 점을 떠올려서

그냥 맨 앞에서 inherit로 상속받게 하면 해결이 된다.

 

반응형

'Front-end > CSS' 카테고리의 다른 글

css 챌린지  (0) 2022.10.05
[CSS] box-sizing  (0) 2022.07.11
[CSS] Gap  (0) 2022.06.13
loading spinner  (2) 2022.02.13
CSS 애니메이션 성능 개선  (0) 2022.01.27
반응형

https://100dayscss.com/

 

css 고수가 되고 싶어서 자료를 찾던중

유용한 사이트를 하나 발견해서 공유해본다~

 

https://100dayscss.com/

 

1일부터 100일까지 챌린지가 준비되어 있는데

아래와 같은 애니메이션을

 

See the Pen Untitled by lodado (@lodado) on CodePen.

직접 만들어보면 된다

 

진짜 어려운데.. 다른사람의 코드를 참고할 수 있으니

많이 배울 수 있는것 같다

 

내 아이디

>> https://100dayscss.com/progress/lodado/

반응형

'Front-end > CSS' 카테고리의 다른 글

font-family 상속 시키기  (0) 2022.11.04
[CSS] box-sizing  (0) 2022.07.11
[CSS] Gap  (0) 2022.06.13
loading spinner  (2) 2022.02.13
CSS 애니메이션 성능 개선  (0) 2022.01.27
반응형

이전에 유저의 명령을 undo, redo 하기 위해서 유저가 실행한 커맨드 하나하나를 기록하여 

스택에 쌓는 작업을 했었다.

 

그런데 다양한 인터렉션을 처리하려고 하다보니 여러 원자적인 연산(create, delete, update)들을 조합해 하나의 커맨드로 기록할 필요성이 생기게 되었다.

 

예를 들어 다음 예시 화면과 같이

부모 노드안에 자식 노드를 추가하려면 한번 추가할때마다 각각 두번의 연산이 필요하다. 

 

1. update 연산 -> 부모 노드(시작 스탭)의 width, height가 커져야 한다. 

2. add 연산 ->  자식 노드를 nodes 배열에 추가해야한다.

 

(undo, redo할땐 두 연산을 하나로 합쳐서 작동해야한다.)

 

이것을 간단한 코드 예시로 보자면 다음과 같다.

	const [nodes, setNodes] = useRecoilState(flowNodesState); // 배열 형태
	const [edges, setEdges] = useRecoilState(flowEdgesState); 
	

	const addChildNode = () => {
		....

		// this should be updateNodes
		setNodes((nodes: Node[]) => 부모 노드 크기 변경)

		// this should be addNodes
		setNodes((nodes: Node[]) => 새로운 자식 추가)
	}

 

setState 함수에서 인자를 이용하면 비록 setState가 비동기라도

기존 state가 아닌 새로 갱신된 state를 인자로 받아 마치 차례대로 실행하는것처럼 사용할 수 있다.

 

식으로 비유하자면

A : 처음 node State

F(x) : update 연산

G(x) : add 연산

 

새로운 상태 : G(F(A))

 

 

문제점 

 

그래서 setState를 히스토리를 기록하기 위해서 command 함수들로 바꾸면

내부적으로는 setState를 커맨드 하나마다 각각 실행하니 기존과 동일하게 실행되지 않을까? 하는 가설을 세웠지만 실제로 실행해보니 기대처럼 작동하지는 않았다.

 

코드 예시

더보기

 

	const [nodes, setNodes] = useRecoilState(flowNodesState);
	const [edges, setEdges] = useRecoilState(flowEdgesState);
	

	const addChildNode = () => {
		....

		updateNodes(부모 노드 크기 변경) 
        
        	addNodes(새로운 자식 노드 추가)
        
	}

 

즉, 내가 원하던건

 

A -> B -> C -> D 연산처럼 A에서 시작해 상태를 순서대로 업데이트 하는것이였다면

 

command들 함수를 이용하면 각각 command 내부에 들어있는 setState들이 서로 연관이 없다고 리엑트가 판단하여 인자값으로 갱신되지 않은 기존의 state를 반환해 A -> B, A -> C,  A->D 처럼 서로 독립적으로 실행이 되어버리는 것이였다.

 

그래서 한가지 아이디어를 냈는데

 

우리가 원하는건 A -> B -> C -> D 연산중에서 최종적으로 히스토리에 필요한것은 

undo했을때 기존의 값으로 되돌릴 A 상태와

redo했을때 되돌릴 D 상태다. 

 

그럼 커맨드들이 node를 인자값으로 받아 node를 return값으로 반환하는 여러가지 함수들을 만들고,

그 함수들을 순서대로 실행하게 이으면 A->B->C->D 연산을 할 수 있지 않을까?

 

그리고 history stack에는 A와 D만 저장하는 식으로 구현하면 될것이다.

 

이를 위해서 함수형 패러다임을 끼워넣어 구현해보기로 해보았따.

 

각 add, remove, update 등 상태를 변경하는 atomic한 함수를 만들고, 

아래의 pipe 함수를 이용해 순서대로 실행하는 식으로 구조를 변경하고,

마지막 갱신된 state를 setState해주는 식으로 구현하면 될것이다 

const pipe = (...fns) => { 
  return (...args) => {
    return fns.reduce( // 입력 받은 fns의 순서를 뒤집는다
      (res, fn) => [fn.call(null, ...res)], // 순서가 뒤집어진 fns 를 오른쪽부터 실행
      args // 초기값으로 받은 파라미터
    )[0];
  }
};

p.s) 위 함수 동작원리에 대한건 https://velog.io/@nakta/FP-in-JS-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-%EC%A0%91%ED%95%B4%EB%B3%B4%EB%8A%94-%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%95%A8%EC%88%98-%EC%BB%B4%ED%8F%AC%EC%A7%80%EC%85%98-%EC%BB%A4%EB%A7%81-s7k2z039vb

를 참고해주세요. 

 

 

이를 간단하게 요약한 코드 예시로 보면 다음과 같다.

const NodesPipeExecution =
		(...fns) => {
			const element: Node[] = nodes.slice();

			return (option?: OptionType) => {
				const options = { ...option, isAtomicCommand: false, doesHistoryBeRecorded: false };

				const { newElement } = pipe(fns)({newElement : element, option : options}); // 함수를 순서대로 실행

				triggerUpdate(newElement) // 실제 history를 기록하고 setState를 반영하는 함수
			};
		};

 

실제 사용 예시

 

NodesPipeExecution(
		updateNodesPipe(부모 노드 크기 업데이트), 
		addNodesPipe(새로운 노드),
)()

 

그런데 이와 같이 함수들을 직접 넘기는 식으로 구현하면 

모든 함수와 파라미터를 알아야 되니 불편하지 않을까? 라는 피드백을 받아서 

 

정해진 인터페이스에 알맞게 넘기면 내부적으로 처리하는 식으로 다시 리펙토링 했다.

 

export type QueryType<T> = ((element: T) => boolean) | { [key: string]: string }[];

export interface AddExecutionPipeType<T> {
	type: "add";
	item: (() => T[]) | T[];
}

export interface RemoveExecutionPipeType<T> {
	type: "remove";
	query: QueryType<T>;
}

export interface UpdateExecutionPipeType<T> {
	type: "update";
	query: QueryType<T>;
	onChange: (element: T) => T;
}

export type ExecutionPipeType<T> = AddExecutionPipeType<T> | RemoveExecutionPipeType<T> | UpdateExecutionPipeType<T>;


const onNodesChange = (functionArray: ExecutionPipeType<Node>[]) => {
		NodesPipeExecution(
			...functionArray.map((func: ExecutionPipeType<Node>) => {
				const { type, item, query, onChange } = func as AddExecutionPipeType<Node> &
					RemoveExecutionPipeType<Node> &
					UpdateExecutionPipeType<Node>;
				switch (type) {
					case "add":
						return addNodesPipe(item);
					case "remove":
						return removeNodesPipe(query);
					case "update":
						return updateNodesPipe(query, onChange);
					default:
						return new Error("developer type error");
				}
			})
		)();
	};

 

실제 사용 예시



onNodesChange([
			{
				type: "update",
				query: 부모 노드 선택,
				onChange: (element: Node) => {
					부모 노드 크기 변경
				},
			},
			{
				type: "add",
				item: 새로운 자식 노드 생성,
			},
		]);

 

함수형 패러다임을 맛만 본거긴 하지만 실제로 해보니 재밌었다.

 

 

reference

 

https://velog.io/@nakta/FP-in-JS-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-%EC%A0%91%ED%95%B4%EB%B3%B4%EB%8A%94-%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%95%A8%EC%88%98-%EC%BB%B4%ED%8F%AC%EC%A7%80%EC%85%98-%EC%BB%A4%EB%A7%81-s7k2z039vb

 

FP in JS (자바스크립트로 접해보는 함수형 프로그래밍) - 함수 컴포지션, 커링

두 번째 글입니다. 함수형 프로그래밍에서는 함수의 조합으로 원하는 값을 만들어 냅니다. 함수의 조합인 함수 컴포지션에 대해서 살펴보도록 하겠습니다. 그리고 커링 기법을 이용해 함수 컴

velog.io

 

반응형

'Front-end > React' 카테고리의 다른 글

react Dialog tab trap 만들기  (1) 2023.05.01
프론트엔드 테스트 정리 요약  (1) 2022.11.30
react-flow에 undo, redo 구현  (0) 2022.09.03
Suspense를 위한 wrapper 만들기  (0) 2022.07.28
react 17(개인 공부용)  (1) 2022.06.09
반응형




일하면서 react-flow(https://github.com/wbkd/react-flow)를 써보던 중

Node와 edge를 기반으로 한 react 시각화 라이브러리



해당 오픈소스 라이브러리에는 유저의 실행 명령을 기록해서 undo, redo하는 기능이 없다는것을 알게 되었다.
그래서 기획 상 필요한 undo, Redo 기능을 어떻게 넣을까 고심하게 되었는데...

배경지식 설명


해당 라이브러리는 node라는 배열과 Edge라는 배열을 가지고

	const [nodes, setNodes] = useRecoilState(flowNodesState); // 배열 형태
	const [edges, setEdges] = useRecoilState(flowEdgesState); 
	

	const addChildNode = () => {
		setNodes((nodes: Node[]) => 새로운 자식 추가)
	}


새로운 UI를 그려줄때마다 리엑트의 setState를 부르는 식으로 구현이 되어있다.

아이디어


setState를 실행할때마다 한 프레임이라고 정의한다면
undo를 하면 이전 프레임으로 상태를 복귀시키고,
Redo하면 다시 undo 했던 명령을 취소해 다시 기존 프레임으로 돌아와야 할것이다.

각 프레임이 변경될때마다 전체 state(node, edge)를 프레임 별로 저장해놓는 것은 메모리 용량 이슈가 발생하는것 같아서
유저가 실행한 커맨드를 순서대로 기억해두고(stack) undo시 해당 실행 커맨드를 역산하고, redo시 다시 실행하자 라는 가설을 세우고 코드를 작성해보았다.

즉, command 단위로 명령어를 저장했다.

모든 명령어는 event라는 단위로 추상화가 가능하다.

이를 위해 유저의 명령을 event라는 하나의 단위로 추상화하고, 이벤트가 실행할때마다 커맨드를 기록하는 식으로 구현하했다.

예를 들어 create, create, create라는 명령이 3번 온다면

우선 undo stack에 [create, create, create] 라는 형식으로 3번 쌓이게 된다.

그리고 undo 명령을 실행하면 undo stack에서 순서대로 명령어를 하나씩 꺼내어 그 명령의 역 커맨드를(역산) 실행하는데

undo시 (역산)
- create 명령어는 delete,
- delete 명령어는 create,
- update 명령어는 rollback

명령어를 실행하고 redo stack에 해당 명령어를 쌓게 된다.

redo 명령시 redo stack에서 명령어를 꺼내서 역으로 실행된 커맨드를 역 실행하기 위해서(부정의 부정은 긍정!)
즉.. 기존 커맨드를 그냥 재실행 하게되는데

redo 시 (재실행)
- create 명령어는 create,
- delete 명령어는 delete
- update 명령어는 update

하게 된다.

매우 간단히 요약된 코드 예시로 보자면

undoStack = [];
redoStack = [];


// 커맨드 기록 
function record(command){
    undoStack.push(command);
}


//undo 실행 
function undo(){
    redoStack.push(undoStack.pop());
}


//redo 실행
function redo(){
    undoStack.push(redoStack.pop());
}


위 두가지 스택이 맞물리는 형태가 된다.

undo, redo 예시


직접 구현하기 까다로웠던 문제였는데 잘 작동하는것 같다.

반응형
반응형

 

 

복잡한 비동기 로직을 사용할때 Suspense를 사용하면 데이터를 사용하는 컴포넌트와 로딩 & 에러 상태를 핸들링하는 방식을 분리시켜 선언적으로 프로그래밍이 가능하다. 

 

react-query나 recoil에서 suspense를 사용하는 방식을 지원해주는데  라이브러리의 지원이 없이 직접 처리하는 방식이 궁금해서 해보았다. 

 

우선 suspense에 대해 인터넷에 찾아보니 나온 코드는 다음과 같다.

 

function wrapPromise(promise: AxiosPromise) {
	let status = "pending";
	let result = {};
	const suspender = promise.then(
		(successResult) => {
			status = "success";
			result = successResult;
		},
		(errorResult) => {
			status = "error";
			result = errorResult;
		}
	);

	return function read() {
		switch (status) {
			case "pending":
				throw suspender;
			case "error":
				throw result;
			case "success":
			default:
				return result;
		}
	};
}

function fetchDataVersion1(requestAPI: Promise) {
	return wrapPromise(requestAPI);
}

const Main = () => {
	return (
			<Suspense fallback={<div>loading...</div>}>
				<Dddd />
			</Suspense>
	);
};

let count = 0;

setInterval(() => {
	count += 1;
}, 1000);

const axiosRequestExample = new Promise((res) => {
	setTimeout(() => {
		res(`${count} is shown`);
	}, 6000);
});

const requestResource = fetchDataVersion1(axiosRequestExample);

const Dddd = () => {
	const data = requestResource();

	return <div>{JSON.stringify(data)}</div>;
};

export default Main;

 

로직을 간단히 설명하자면, 

 

function wrapPromise(promise: AxiosPromise) {
	let status = "pending";
	let result = {};
	const suspender = promise.then(
		(successResult) => {
			status = "success";
			result = successResult;
		},
		(errorResult) => {
			status = "error";
			result = errorResult;
		}
	);

	return function read() {
		switch (status) {
			case "pending":
				throw suspender;
			case "error":
				throw result;
			case "success":
			default:
				return result;
		}
	};
}

비동기 데이터를 처리중인 상태에는 상위 컴포넌트로 promise를 넘겨서 Suspense가 처리하게 만들고,
(동일하게 error도 상위 컴포넌트로 넘겨서 ErrorBoundary(직접 구현해야함)가 처리하게 함)

데이터가 도착하면 렌더링 하는식으로 

 

Suspense를 이용하면 데이터를 불러오는 event(what)과 어떻게 처리하는 로직(how)을 분리시켜 선언적으로 처리하기 쉽게 만들어주는 강력한 도구가 될 수 있다.

 

그런데 위 코드는 실제로 사용하려고 하니 다음과 같은 기능이 부족했다.

 

1. 캐싱처리로 동일한 데이터는 캐싱처리해서 불러오는 시간을 줄임

2. 필요시 캐싱 처리를 무효화하고 새로운 데이터를 받아올 수 있어야함.

3. 여러 컴포넌트에서 재사용할 수 있어야함. 

 

그래서 다음의 블로그를 참고해서 위 조건을 만족하게 새로 만들어보았다.

 

https://coffeeandcakeandnewjeong.tistory.com/56

 

최종 구현형태는 다음과 같다.

 

let count = 1;

const TEST_URL = () => {
	console.log(`https://jsonplaceholder.typicode.com/todos/${count}`);

	return `https://jsonplaceholder.typicode.com/todos/${count}`;
};

setInterval(() => {
	count += 1;
}, 1000);

const Main = () => {
	return (
		<ErrorBoundary fallback={<div>error</div>}>
			<Suspense fallback={<div>213</div>}>
				<Dddd />
			</Suspense>
		</ErrorBoundary>
	);
};

const Dddd = () => {
	const { data, refresh } = useSuspense("useSuspense", () => axios(TEST_URL()), [123, 456, 789, 1011]);

	return (
		<div>
			<button type="button" onClick={() => refresh()}>
				refresh
			</button>
			<Link to="/login">to login</Link>
			{JSON.stringify(data)}
		</div>
	);
};

 

그런데 실제 구현물을 보니 react-query와 비슷하게 만든거 같다;;

이렇게 되면 위 코드를 실제로 사용하기는 애매해진거 같은 기분이다.

 

top-down 방식으로 위 코드를 파해쳐보자. 

type depsType = number[] | string[];
type axiosType = () => Promise<any>;

function useForceUpdate(key: string) {
	const [value, setValue] = useState(0);
	return () => {
		asyncManager.delete(key);
		setValue(Date.now());
	};
}

const useSuspense = (key: string, getPromiseCandidate: axiosType, deps: depsType = []) => {
	const resetFunction = useForceUpdate(key);

	const refresh = () => {
		resetFunction();
	};

	useEffect(() => {
		return () => asyncManager.delete(key);
	}, []);

	const { nextValue, force } = depsMemorization(key, getPromiseCandidate, deps); //deps 비교
	const { status, suspender, result } = getPromiseData(key, nextValue, force); // Promise 처리 

	if (status === "pending") {
		throw suspender;
	}

	if (status === "error") {
		throw result;
	}

	return { data: result, refresh };
};

 

가장 먼저 useSuspense hook부터 살펴보자.

 

맨 처음 소개되었던 코드에 다음과 같은 기능이 추가됐다.

 

1. depsMemorization - 캐싱처리(key와 deps 비교를 통해)

2. getPromiseData - Promise 처리 및 key값에 따라 메모라이징 

3. refresh - 캐싱 무효화 & refetch 후 re-rendering

 

status에 따라 throw & return 하는 방식은 동일하다.

다만 asyncManager라는 새로운 객체를 추가했고 해당 객체로 캐싱처리 관련을 해줄 예정이고(crud 가능),

key가 동일하고 deps가 동일하다면 한 컴포넌트에서 fetch를 받아왔으면 캐싱처리해 여러 컴포넌트에서 동일하게 사용할 수 있도록 구현했다.

 

다음은 depsMemorization 코드이다.

 

const depsMemorization = (key: string, axiosRequest: axiosType, deps: depsType) => {
	if (!deps || !Array.isArray(deps) || deps.length === 0) return { nextValue: axiosRequest(), force: true };

	if (asyncManager.has(key)) {
		return asyncManager.update(axiosRequest, deps, key);
	}

	return asyncManager.mount(axiosRequest, deps, key);
};

사실 deps란 기능이 필요없다면 key의 유무만 판단해서 구현해줘도 된다.

그게 구현하기 더 심플한거 같기도 하다.

 

useEffect등의 deps 배열과 비슷한 역할을 하게 구현이 되어있다.

deps를 비교해 동일하다면 캐싱 값을 리턴하고 아니라면 key값을 기준으로 새로 캐싱해주는 형태로 구현이 되어있다.

 

다음은 Promise를 핸들링하는 코드인 getPromise이다.

 

const getPromiseDataClosure = () => {
	const promiseMem = new Map();

	return (key: string, axiosRequest: Promise<any>, force: boolean) => {
		if (promiseMem.has(key) && !force) {
			return promiseMem.get(key);
		}
		const promiseData = {
			promise: axiosRequest,
			status: "pending",
			result: null,
			suspender: axiosRequest.then(
				(response) => {
					promiseData.status = "success";
					promiseData.result = response;
				},
				(error) => {
					promiseData.status = "error";
					promiseData.result = error;
				}
			),
		};

		promiseMem.set(key, promiseData);
		return promiseData;
	};
};

const getPromiseData = getPromiseDataClosure();

기능은 심플한데 아까 맨 처음 나온 코드에 key값을 통한 메모리제이션 기능이 추가되었다.

 

1초마다 count가 올라가고, refresh 누를때마다 testurl/{count} 주소 로 request를 보내는 방식으로 구현해봤는데 실험 결과 잘 작동하는것 같다.

 

최종 코드는 다음 주소에서 볼 수 있다.

 

https://codesandbox.io/s/zealous-hugle-ozz7j0?file=/src/App.tsx

 

 

반응형

'Front-end > React' 카테고리의 다른 글

react-flow에 undo, redo 구현 (2) - 함수형 패러다임  (0) 2022.09.17
react-flow에 undo, redo 구현  (0) 2022.09.03
react 17(개인 공부용)  (1) 2022.06.09
React18(개인 공부 메모용)  (0) 2022.06.09
react TMI  (0) 2022.04.13
반응형

자바스크립트로 큐를 구현할때 Array 배열의 함수인 shift()를 이용해서 구현하는 경우가 있습니다.

 

코드 예시

    class Queue1{
        
        constructor(){
            this.arr = [];
        }
        
        isEmpty(){
            return this.arr.length === 0;
        }
        
        enqueue(value){
            this.arr.push(value);
        }
        
        dequeue(){
            
            if(this.isEmpty()){
                return null;
            }
            
            return this.arr.shift();
        }
    }
    
    const length = 100000;
    const que1 = new Queue1();
    const time = new Date();
    
    for(let i=0; i<length; i++){
        que1.enqueue(i);
    }
    
    for(let j=0; j<length; j++){
        que1.dequeue();
    }
    
    console.log(new Date() -time); // 2125ms

 

큐를 구현할때 해당 방식이 가장 쉽고 간편합니다. 

다만 array의 shift() 함수의 시간 복잡도가 O(n)이라(맨 앞의 값을 pop 시키고 한칸씩 앞으로 당기는 식으로 구현되어 있음)

데이터의 크기가 많아질수록 느려지는 문제가 발생할 수 있습니다.

 

그래서 다른 방법을 찾아봐야 하는데 대안으로 다음과 같은 방식을 생각해 보았습니다.

 

1. 배열 + 원형 큐

 

->  크기 limit 문제

 

2. stack 2개로 1개의 큐 만들기

 

스택 두개에서 하나에 데이터를 쭉 쌓고, dequeue 요청이 들어오면 다른 스택에 전부 pop하기(그럼 역순으로 쌓임)

-> pop할때 오버헤드 발생

 

3. 링크드 리스트 사용

 

이 중 가장 간편한 방법이자 성능이 좋은 방법이 단방향 링크드 리스트를 이용하는 방법입니다.

 

코드로 봅시다.

 

코드 예시

 

    class Queue2{
        
        constructor(){
            this.head = this.cursor = undefined;
        }
        
        isEmpty(){
            
            if(!this.head){
                return true;
            }
            
            return false;
        }
        
        enqueue(value){
            
            if(!this.head){
                this.head = this.cursor = new Node(value);
                return;
            }
            
            const node = new Node(value);
            this.cursor.next = node;
            this.cursor = node;
        }
        
        dequeue(){
            
            if(this.isEmpty()){
                return null;
            }
            
            const tmp = this.head;
            this.head = this.head.next;
            
            if(tmp === this.cursor){
                this.cursor = this.head;
            }
            
            return tmp.value;
        }
    }
    
    
    
    const length = 100000;
    const que2 = new Queue2();
    const time = new Date();
    
    for(let i=0; i<length; i++){
        que2.enqueue(i);
    }
    
    for(let j=0; j<length; j++){
        que2.dequeue();
    }
    
    console.log(new Date() -time); // 16ms

 

매우 간단한 단방향 링크드 리스트를 사용한 방식입니다.

코드를 요약하자면 

 

head 변수가 맨 앞 노드를 가리키고, cursor 변수가 맨 뒤를 가리킵니다. (포인터 느낌?)

 

push때엔 노드를 맨 뒤에 추가하고 cursor를 해당 노드로 변경,

pop땐 head에서 노드를 하나씩 꺼내고 한칸 뒤로 head 커서를 이동하는식으로 간단히 구현이 되어 있습니다.

해당 방식으로 2초 가량 걸리던 코드가 16ms 정도 걸리는 정도로 성능이 개선되었습니다.

 

js에는 STL같은 라이브러리가 없으니 직접 큐를 구현할 일이 생길때 쓰면 좋을듯 싶습니다. 

반응형
반응형

Sketch

 

가장 초창기에 나온 전통적인 디자인 툴

(Sketch는 맥OS에서만 사용가능)

Sketch는 클라우드 서버를 제공하고 로컬 저장 가능

Sketch는 클라우드 스토리지 서비스를 이용해 다른 사용자와 파일을 공유하고 편집할 수 있는 기능을 제공하나 실시간 협업 방식은 아니다.

 

Figma (디자이너 툴 - 가장 추천)

협업을 위한 디자인 툴(2021년 기준 선호도 1위)

 

프레이밍 전략: 디자인이 아닌 협업에 초점

여러 사람이 웹에서 실시간으로 동시 작업하는 모습을 강조

 

장점

 

웹 기반 동시에 아트보드 편집 -> 디자이너가 작업하는 동안 개발자가 기다리는 병목 현상 X

디자인의 css 그대로 copy 가능

 

단점

인터넷이 필수 -> 보안 문제 발생 가능

한글 관련 자잘한 버그

 

디자인 시스템 (방법론)

그림 1) 라인의 디자인 시스템 예시

 

개발자 사이에 협업을 위해서 그라운드 룰로 컨벤션을 정하듯이 디자이너 <=> 개발자,

혹은 디자이너끼리 커뮤니케이션 하기 위해서 정해놓은 방법론

 

도입 이전의 문제 제기

 

일관되지 않은 디자인 -> 제품의 사용성을 떨어트림

비효율적인 작업 -> 동일 코드 & 디자인의 중복 작성 문제

확장성 부족 -> 로직에 집중해야할 시간을 UI에 집중

 

해결 방안

 

정해진 디자인 패턴 & 컴포넌트를 재사용해 제품을 구축하여 사용자 입장에서 일관되고 차별화된 경험을 하도록 하는 가이드라인

개발자 입장에서 제품을 구축할때 컴포넌트를 재사용해서 구축 & 개선하는데 걸리는 시간 단축

 

구현 해놓으면 이후엔 재사용으로 작업 속도가 빨라질것이라 예상 가능

시스템 자체가 중요한게 아니라 쓸만한 “룰”이 중요

 

예시 사례)

Figma - material UI(https://www.figma.com/file/aNN74suNWBb0uFGW3lxV7Q/Material-Me-(preview)?node-id=10276%3A257885)

Figma - buzzvil (https://www.figma.com/file/oWpE6CAuRW9fdCRziPGrnu/Apps--—-Buzzvil-Design-System-(Community)?node-id=4809%3A183)

 

storybook (개발 툴)

컴포넌트 단위의 UI 개발 환경을 지원하는 도구

 

주 사용 목적

개발된 컴포넌트의 문서화

개발 작업물 공유(개발자<->개발자, 디자이너 등)

협업시 다른 사람들에게 현 'context' 공유가 주 목적

내가 아닌 남을 위한 작업

반응형

+ Recent posts