Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

스택 컴포넌트 #2

Merged
merged 14 commits into from
Oct 5, 2023
Merged

스택 컴포넌트 #2

merged 14 commits into from
Oct 5, 2023

Conversation

WaiNaat
Copy link
Member

@WaiNaat WaiNaat commented Oct 2, 2023

npm

스토리북

코드샌드박스도 하고싶었는데 거기는 자꾸 import 가 이상하게되네요..

일단 피움 로컬에서는 돌아간다는 마법의 문장 적고 갑니다..

제 로컬에서는 되는데요

특이사항

컴파운드 버렸습니다

대신에 Stack에서 하위 요소에 투명도 변환용 애니메이션을 하나 넣긴 합니다

사용법은 기존이랑 최대한 비슷하게 하려고 노력했어요

중간에 추상화병 걸려서 살짝 기능들이 늘어나긴 했지만요..
일단 prop으로 숫자를 받아서 보여주는 방식은 똑같이 유지했습니다.
스토리북에 잘 써놓았으니 한번 구경해주세요 👍

이제 자식 컴포넌트의 높이는 알아서 계산합니다

ResizeObserver 사용

화면 크기가 변하더라도 애니메이션이 최대한 덜 꼬이게 하기 위해서 사용했어요

random offset을 준 이유

css 애니메이션은 딱 한 번만 실행된다는 특징이 있는데 offset이 달라지면 keyframe도 아예 다른 걸로 취급해서 재실행하는 원리입니다.
스택 애니메이션은 새로 들어온 컴포넌트의 높이를 기준으로 돌아가는데 새로 들어온 컴포넌트의 높이가 같다면 같은 keyframe으로 취급해서 애니메이션이 안 돌아가더라구요 😢 다른 방법이 생각나지 않아서 일단 돌아가게만 만들었습니다

npm은 @pium/stack-component입니다.

두 분 다 npm 단체 초대 드렸는데 메일로 가지 않았을까 싶네요

@WaiNaat WaiNaat self-assigned this Oct 2, 2023
Copy link

@hozzijeong hozzijeong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멋있습니다... 감동이네요!!

궁금한거 위주로 코멘트 남겼습니다!

고생하셨어요~

const resizeObserver = new ResizeObserver((entries) => {
const [self] = entries;
const { height: resizedHeight } = self.target.getBoundingClientRect();
flushSync(() => setHeight(({ current }) => ({ previous: current, current: resizedHeight })));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C

fluchSync가 아마 동시성에서 보류중인 작업들을 제외하고 바로 렌더링하도록 설정하는 것으로 알고 있는데 이 함수를 적용하신 이유가 있나요?

Most of the time, flushSync can be avoided. Use flushSync as last resort.

리액트 공식문서에서도 최대한 사용을 지양하라고 하는데, 사용하신 이유가 궁금합니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flushSync가 없으면 바뀐 height의 적용이 미묘하게 느려서 아래 짤의 3번이랑 5번 상자 내려올때처럼 가끔씩 고장이 나더라구요

stack_flushSync

원래는 useLayoutEffect로 해결했었는데 resizeObserver를 사용하게 되면서 useLayoutEffect가 필요 없어져 다시 flushSync로 해결했습니다

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resizeObserver를 사용하지 않고 layoutEffect로 돌아가는 것에 대해서는 어떻게 생각하시나요??

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러가지를 시도해 봤는데 자연스럽게 스택이 내려오지 않네요... 답답 그 자체입니다...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😭😭😭

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useLayoutEffect를 쓴다면 아래처럼 children이 바뀌었을 때 높이를 다시 계산하도록 유도할 수 있는데요!

useLayoutEffect(() => {
  if (container.current) {
    const { height: resizedHeight } = container.current.getBoundingClientRect();
    setHeight(({ current }) => ({ previous: current, current: resizedHeight }));
  }
}, [children]);

이 경우 아래 짤처럼 화면 크기 변화에 아예 대응을 못한다는 문제가 있습니다. 저도 처음에 useLayoutEffect 보고 물개박수쳤다가 이 문제를 어떻게 해결해야 좋을지 모르겠어서 다른 방법으로 넘어가게 되었어요 😭

stack_layout_effect

Comment on lines 18 to 25
from { transform: translate3d(0, ${height}, 0) }
to { transform: translate3d(0, 0, 0) }
`;

const fall = (height: StackWrapperProps['$newChildHeight']) => keyframes`
from { transform: translate3d(0, calc(-1 * ${height}), 0) }
to { transform: translate3d(0, 0, 0) }
`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C

여기서 3d를 사용하신 이유가 있을까요?!?! 쌓이는 스택이 2차원이라고 생각을 해서 3d를 사용하신 이유가 궁금합니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어디서 translate3d는 translateY와는 다르게 하드웨어 가속으로 GPU를 써서 좋다는 말을 들어서 사용했습니다.
근데 찾아보니까 이 블로그 말로는 transform 자체가 GPU를 쓴다고 하네요.. 로컬에서 해본 결과 translate3d랑 translateY 모두 GPU 자원을 먹었습니다

);
};

export default Stack;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C

해당 컴포넌트를 memo로 반환하지 않는데, useMemo를 사용하면 효과가 있나요?!?! 저는 같이 써야 하는걸로 알고 있습니다!!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그거는 아마도 '해당 컴포넌트의 리렌더링 자체를 건너뛰고 싶은 경우'라고 생각해요 (공식 문서: Should you add useMemo everywhere? 의 2번 내용)

하지만 여기에 사용된 useMemo는 리렌더링 건너뛰기나 최적화와는 관련 없이 단순히 리렌더링에도 처음 정했던 무작위 값들을 기억하라는 의도로 쓴 거라 상관은 없다고 봅니다

근데 또 찾아보니까 같은 문서에서 성능 최적화 목적이 아니면 useMemo보다는 state나 ref를 사용하는 게 더 나을 수 있다고는 하네요.. 너무 어렵습니다. 처음에는 ref를 쓰려다가 ref 렌더링에 쓰지 않는 값이라고 해서 useMemo로 옮긴 거였거든요

무작위 값을 렌더링에 사용하는 게 컴포넌트의 순수성을 해치진 않을까 하는 걱정도 드네요 strict mode를 통과하는 걸 보면 순수하다고 판단하는 것 같기도 하지만 컴포넌트가 mount될 때마다 무작위값은 변하니까요

useMemo보다는 useState를 쓰거나 아예 프로그램이 처음 시작할 때 전역 상수처럼 무작위 배열을 만들고 그걸 사용하는 게 나을 수 있겠다는 생각도 드는데 어떻게 생각하시나요??

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하지만 여기에 사용된 useMemo는 리렌더링 건너뛰기나 최적화와는 관련 없이 단순히 리렌더링에도 처음 정했던 무작위 값들을 기억하라는 의도로 쓴 거라 상관은 없다고 봅니다

저도 이 부분은 공감 되네요~

확실히 계산이 무거워서 메모하는 느낌도 아니고, 꼭 ref로 유지해야만 하는 값도 아니긴 하네요!
생각해보니 offset을 미세하지만 각각 다르게 주는 것이 목적이라면 랜덤이 아니라 그냥 상수로 만들어서 줘도 될까요?

Copy link
Member Author

@WaiNaat WaiNaat Oct 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const randomOffsets = getUniqueRandomFloatArray(7);
const Stack = (props) => {
// 후략

해보진 않았는데 이렇게 상수로 만들어서 써도 되긴 합니다

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 생각해보니 useMemo를 사용해도 괜찮을 것 같긴 하네요. 동적으로 결정되는 값들을 캐싱하는데 있어서 괜찮은 선택지라고 생각합니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 고민해봤는데 아무래도 useMemo 자체를 버리고 Stack 밖으로 빼는 게 제일 어울린다고 생각해요
뭔가 동적으로 결정되는 건 맞지만, Stack을 여러 개 사용할 때 randomOffsets 배열 내부의 값들이 바뀌어야 할 이유를 떠올리지 못했습니다

Copy link

@bassyu bassyu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSDoc 인상적이네요!
코드를 조금 보다보니 원래의 구조에서 이렇게 추출하기 꽤 어려웠을 것 같은데 고생하셨습니다... 👍

질문만 조금 있습니다! 제가 잘 이해하지 못한 것 같은데, animationOffset역할이랑 랜덤인 이유가 궁금해요~

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외적인 숫자 대응도 꼼꼼하게 하셨네요! 😮👍

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fall은 Wrapper에 있는데, rise는 lastChildAnimation에 있게된 히스토리가 궁금해요~

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rise는 맨 마지막으로 들어오는 친구만 위로 올려주면 돼서 child에 넣었어요
fall은 처음으로 들어오는 친구뿐만이 아니라 그 아래 원래 있던 애들까지 다 자연스럽게 밑으로 밀어야 해서 한번에 처리하기 위해 전체 Wrapper에 넣었습니다

Copy link

@bassyu bassyu Oct 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞네요! 이해됐습니다 👍

Copy link
Member Author

@WaiNaat WaiNaat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에는 animationOffset 관련해서 적겠습니당

  1. css 애니메이션은 한 번 실행하면 reset이 안 돼요
  2. fall 애니메이션은 전역 Wrapper에 있습니다

이 두 가지 상황 속에서 stack에 들어올 컴포넌트 다섯 개의 높이가 각각 25픽셀로 완전히 똑같고, offset이 없다고 할게요
이러면 $newChildHeight가 항상 똑같게 나오고, 스타일드 컴포넌트는 prop이 바뀌질 않으니 항상 똑같은 class를 만들어서 html에 넣습니다. 그래서 html 입장에서는 같은 class에 같은 애니메이션이니까 맨 처음에 떨어질 때만 애니메이션이 나오는 문제가 있었어요

(움짤 참고)
stack_without_random

그래서 '같은 높이의 요소들이 들어와도 서로 다른 애니메이션을 보여주는 법'을 고민했는데 마땅한 방법이 떠오르지 않아서 '눈에 보이지 않을만큼 미세하게 다른 애니메이션'을 보여주기로 했어요.

그래서 무작위 값으로 된 offset으로 최대한 겹치지 않게 만들고자 하는 의도였어요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rise는 맨 마지막으로 들어오는 친구만 위로 올려주면 돼서 child에 넣었어요
fall은 처음으로 들어오는 친구뿐만이 아니라 그 아래 원래 있던 애들까지 다 자연스럽게 밑으로 밀어야 해서 한번에 처리하기 위해 전체 Wrapper에 넣었습니다

Comment on lines 18 to 25
from { transform: translate3d(0, ${height}, 0) }
to { transform: translate3d(0, 0, 0) }
`;

const fall = (height: StackWrapperProps['$newChildHeight']) => keyframes`
from { transform: translate3d(0, calc(-1 * ${height}), 0) }
to { transform: translate3d(0, 0, 0) }
`;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어디서 translate3d는 translateY와는 다르게 하드웨어 가속으로 GPU를 써서 좋다는 말을 들어서 사용했습니다.
근데 찾아보니까 이 블로그 말로는 transform 자체가 GPU를 쓴다고 하네요.. 로컬에서 해본 결과 translate3d랑 translateY 모두 GPU 자원을 먹었습니다

const resizeObserver = new ResizeObserver((entries) => {
const [self] = entries;
const { height: resizedHeight } = self.target.getBoundingClientRect();
flushSync(() => setHeight(({ current }) => ({ previous: current, current: resizedHeight })));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flushSync가 없으면 바뀐 height의 적용이 미묘하게 느려서 아래 짤의 3번이랑 5번 상자 내려올때처럼 가끔씩 고장이 나더라구요

stack_flushSync

원래는 useLayoutEffect로 해결했었는데 resizeObserver를 사용하게 되면서 useLayoutEffect가 필요 없어져 다시 flushSync로 해결했습니다

);
};

export default Stack;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그거는 아마도 '해당 컴포넌트의 리렌더링 자체를 건너뛰고 싶은 경우'라고 생각해요 (공식 문서: Should you add useMemo everywhere? 의 2번 내용)

하지만 여기에 사용된 useMemo는 리렌더링 건너뛰기나 최적화와는 관련 없이 단순히 리렌더링에도 처음 정했던 무작위 값들을 기억하라는 의도로 쓴 거라 상관은 없다고 봅니다

근데 또 찾아보니까 같은 문서에서 성능 최적화 목적이 아니면 useMemo보다는 state나 ref를 사용하는 게 더 나을 수 있다고는 하네요.. 너무 어렵습니다. 처음에는 ref를 쓰려다가 ref 렌더링에 쓰지 않는 값이라고 해서 useMemo로 옮긴 거였거든요

무작위 값을 렌더링에 사용하는 게 컴포넌트의 순수성을 해치진 않을까 하는 걱정도 드네요 strict mode를 통과하는 걸 보면 순수하다고 판단하는 것 같기도 하지만 컴포넌트가 mount될 때마다 무작위값은 변하니까요

useMemo보다는 useState를 쓰거나 아예 프로그램이 처음 시작할 때 전역 상수처럼 무작위 배열을 만들고 그걸 사용하는 게 나을 수 있겠다는 생각도 드는데 어떻게 생각하시나요??

굳이 3d를 쓰지 않아도 GPU를 사용해서 변경
Copy link

@hozzijeong hozzijeong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flushSync를 사용하지 않고 다른 방법이 있지 않을까 하고 여러가지를 시도해 봤는데 이게 쉽지 않네요.. 웬만하면 참새가 올려준 예시대로 자식 컴포넌트가 height를 차지하고 그 다음에 자식이 내려오는 방식이라 확실히 부자연스럽긴 합니다... 진짜 쉽지 않네요...


const container = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState({ previous: 0, current: 0 });
const randomOffsets = useMemo(() => getUniqueRandomFloatArray(7), []);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R

이 값은 7이 고정인가요? 자식들의 사이즈에 변해야 하지 않나 하고 조심스럽게 생각해 봅니다. 다음과 같이 변경해보면 어떤가요?!

Suggested change
const randomOffsets = useMemo(() => getUniqueRandomFloatArray(7), []);
const childArray = Children.toArray(children);
const randomOffsets = useMemo(() => getUniqueRandomFloatArray(childArray.length), []);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 같은 크기의 자식들이 연속하는 경우를 처리하기 위한 로직이라 길이가 2만 되어도 충분하긴 합니다. 오히려 배열의 길이에 상관없이 진짜 운이 나쁘면 아래와 같은 문제가 생길 수도 있긴 해요ㅋㅋㅋ

높이 offset
20.2 0.8 21.0
20.6 0.4 21.0

7로 정한 이유는 피움 팀이 일곱 명이어서입니다 😉

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗! 그런 의미가...ㅎ

const resizeObserver = new ResizeObserver((entries) => {
const [self] = entries;
const { height: resizedHeight } = self.target.getBoundingClientRect();
flushSync(() => setHeight(({ current }) => ({ previous: current, current: resizedHeight })));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러가지를 시도해 봤는데 자연스럽게 스택이 내려오지 않네요... 답답 그 자체입니다...

Copy link
Member Author

@WaiNaat WaiNaat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 randomOffset은 제 임의로 컴포넌트 밖으로 빼봤습니다.. 혹시 좀 아닌 것 같으면 언제든지 알려주세요!!


const container = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState({ previous: 0, current: 0 });
const randomOffsets = useMemo(() => getUniqueRandomFloatArray(7), []);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 같은 크기의 자식들이 연속하는 경우를 처리하기 위한 로직이라 길이가 2만 되어도 충분하긴 합니다. 오히려 배열의 길이에 상관없이 진짜 운이 나쁘면 아래와 같은 문제가 생길 수도 있긴 해요ㅋㅋㅋ

높이 offset
20.2 0.8 21.0
20.6 0.4 21.0

7로 정한 이유는 피움 팀이 일곱 명이어서입니다 😉

const resizeObserver = new ResizeObserver((entries) => {
const [self] = entries;
const { height: resizedHeight } = self.target.getBoundingClientRect();
flushSync(() => setHeight(({ current }) => ({ previous: current, current: resizedHeight })));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😭😭😭

);
};

export default Stack;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 고민해봤는데 아무래도 useMemo 자체를 버리고 Stack 밖으로 빼는 게 제일 어울린다고 생각해요
뭔가 동적으로 결정되는 건 맞지만, Stack을 여러 개 사용할 때 randomOffsets 배열 내부의 값들이 바뀌어야 할 이유를 떠올리지 못했습니다

const resizeObserver = new ResizeObserver((entries) => {
const [self] = entries;
const { height: resizedHeight } = self.target.getBoundingClientRect();
flushSync(() => setHeight(({ current }) => ({ previous: current, current: resizedHeight })));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useLayoutEffect를 쓴다면 아래처럼 children이 바뀌었을 때 높이를 다시 계산하도록 유도할 수 있는데요!

useLayoutEffect(() => {
  if (container.current) {
    const { height: resizedHeight } = container.current.getBoundingClientRect();
    setHeight(({ current }) => ({ previous: current, current: resizedHeight }));
  }
}, [children]);

이 경우 아래 짤처럼 화면 크기 변화에 아예 대응을 못한다는 문제가 있습니다. 저도 처음에 useLayoutEffect 보고 물개박수쳤다가 이 문제를 어떻게 해결해야 좋을지 모르겠어서 다른 방법으로 넘어가게 되었어요 😭

stack_layout_effect

Copy link

@hozzijeong hozzijeong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멋져요~ 고생 많이하셨습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants