반응형

프론트엔드 코드의 특성

  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' 공유가 주 목적

내가 아닌 남을 위한 작업

반응형
반응형

태그

 

Map tag

HTML CSS JavaScript MDN infographic

 

HTML <map> 요소 <area> 요소와 함께 이미지 맵(클릭 가능한 링크 영역)을 정의할 때 사용

 

https://developer.mozilla.org/ko/docs/Web/HTML/Element/map

 

속성 

role

 

<!-- role example -->
<li role="menuitem">Open file…</li>

Role 은 HTML 요소의 역할을 약속

기본 tag만으로 해당 요소의 역할을 정의하기 어려울때 사용

 

        <div role="button">Place Order</div>

해당 <div>의 작성자가 버튼에 기대되는 키보드 인터랙션을 제공하는 JavaScript도 구현했다는 약속

(반대로 아래에서 설명할 ARIA 역할(role)은 브라우저가 키보드 동작이나 스타일링을 제공하지 않습니다.)

 

 

area 계열

 

속성 (Property) 는 해당 요소의 특징이나 상황을 정의하며 aria- 라는 접두사를 사용

 

 

 

aria-label

<button aria-label="menu" class="button"></button>

aria-label 은 우리가 흔히 알고 있는 Label 목적을 위한 속성

특정 요소에 대한 설명을 그대로

 

 

... 공부하는대로 계속 업데이트중 

 

 

CSS들 

 

https://www.notion.so/css-851342264b4c4bde9f96a6f8f8e765bf

 

 

 

 

reference

 

https://bcp0109.tistory.com/348

 

웹 접근성과 WAI-ARIA

Overview HTML 페이지를 만들 때 고려해야 하는 것 중 하나가 웹 접근성입니다. 웹 접근성이란 시각장애인들이 웹 페이지를 원활하게 이용할 수 있도록 알려주는 가이드라인이라고 생각하면 됩니다

bcp0109.tistory.com

https://mulder21c.github.io/aria-practices/

 

WAI-ARIA 작성 방법 1.2

유저 인터페이스가 적절한 접근 가능한 이름을 제공하는데 사용될 수 있는 보이는 텍스트를 포함하는 경우, 접근 가능한 이름에 대한 보이는 텍스트를 사용하는 것이 유지보수를 간소화하고,

mulder21c.github.io

 

반응형

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

jest가 느린이유  (0) 2022.02.18
반응형

tabIndex

 

tabIndex 전역 특성은 요소가 포커스 가능함을 나타내며,

Tab키를 사용한 연속적인 키보드 탐색에서 어느 순서에 위치할지 지정해준다.

 

tabIndex는 3개의 상태를 가진다.

 

음의 정수값 : 연속 키보드 탐색(이하 tab)으로 접근할 수 없는 값

 

0 : tab을 사용해 접근할 수 있는값, 위에서 아래 순서대로 접근 가능해짐

 

양의 정수값 : tab을 사용해 접근 가능하며 숫자가 큰 순서대로 우선순위가 정해진다. 최대값은 32767이다.

0부터 시작해서 다음에 1이 있고, 그다음 2가 되고 이런 식이다.

 

대화형 컨텐츠(<a>, <button>, <details>, <input>, <select>, <textarea> 등)은 tabIndex의 기본값이 0이고,

비대화형 컨텐츠(div 등)은 기본값이 -1이라 지정 해주지 않으면 tab키로 접근하지 않는다.

 

예시)

 

tabIndex test

1
2
10
1

(tistory에서 다른 tab때문에 엉키게 보임)

 

코드) 

 

<h2>tabIndex test</h2>
<div>
   <div>1</div>
   <div tabindex="3">2</div>
   <div tabindex="1"> 10</div>
   <button tabindex="2" type="button">abc</button>
   <div>1</div>
</div>

 

tabIndex를 임의대로 사용하면 사용자 입장에서 의도치 않은 결과를 낼 수 있는것 같으니

가능하면 양수 값 사용은 자제하는게 좋을 것 같다.

 

modal 열릴때 다른 곳 tab focus 방지를 위해 tabIndex를 활용하면 좋다는데

이건 한번 써보고 이어 쓰기로.. 

 

reference 

 

https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/tabindex

 

tabindex - HTML: Hypertext Markup Language | MDN

tabindex 전역 특성은 요소가 포커스 가능함을 나타내며, 이름에서도 알 수 있듯, 주로 Tab 키를 사용하는 연속적인 키보드 탐색에서 어느 순서에 위치할지 지정합니다.

developer.mozilla.org

 

https://mygumi.tistory.com/53

 

웹 접근성 tabindex 속성 :: 마이구미

이번 글은 tabindex 속성에 대한 웹 접근성에 대해 알아볼 것이다. 참고 자료 - http://nuli.navercorp.com/sharing/blog/post/1132726 tabindex 를 활용한 하나의 테크닉 - https://mygumi.tistory.com/372 tab..

mygumi.tistory.com

 

반응형

+ Recent posts