[고급 레이아웃 컴포넌트] 우스(김영길) 미션 제출합니다.#89
Conversation
hozzijeong
left a comment
There was a problem hiding this comment.
안녕하세요 우스~! 우스의 학습 방법과 깊이에 대해 감탄을 하고 갑니다... 너무 멋있어요 👍
개인적으로 node_modules의 설정을 바꾼 것이 과감하면서도 멋있으면서도 걱정되네요... 그래도 멋있는 시도라고 생각합니다! (저는 생각도 못해봤을 것 같아요)
코멘트 몇 개 남겼는데 확인 부탁드립니다!!
| ...{ | ||
| ...child.props, | ||
| }, |
There was a problem hiding this comment.
| ...{ | |
| ...child.props, | |
| }, | |
| ...child.props, |
위와 같이 변경할 수 도 있지 않을까요?
There was a problem hiding this comment.
디테일하게 봐주셔서 감사합니다 👍
클린이 제안해준 코드로 고쳐볼 수 있을 것 같아요 💯
| { css, as, children, ...rest }: MasonryLayoutProps<T>, | ||
| ref: PolymorphicRef<T> | ||
| ) { | ||
| const childArray = Array.isArray(children) ? children : [children]; |
There was a problem hiding this comment.
여기서 childArray의 type이 any가 나타나는데, 어느 타입이든 상관이 없는 것일까요?
Masonry의 children에 할당한 값이 ReactElement속성이 아닌 일반적인 string 형태가 오게 된다면 어떻게 되는지 궁금합니다!
There was a problem hiding this comment.
수정한 코드입니다
수정 전
onst MasonryLayout: MasonryLayoutComponent = forwardRef(function MasonryLayout<
T extends ElementType = 'div'
>(
{ css, as, children, ...rest }: MasonryLayoutProps<T>,
ref: PolymorphicRef<T>
) {
const childArray = Array.isArray(children) ? children : [children];
return (
<S.Component as={as} ref={ref} style={css} {...rest}>
{childArray.map((child) => (
<MasonryLayoutItem {...rest}>
{createElement(child.type, child.props)}
</MasonryLayoutItem>
))}
</S.Component>
);
});수정 후
const MasonryLayout: MasonryLayoutComponent = forwardRef(function MasonryLayout<
T extends ElementType = 'div'
>(
{ css, as, children, ...rest }: MasonryLayoutProps<T>,
ref: PolymorphicRef<T>
) {
const isChildrenArray = Array.isArray(children);
const childArray: ReactElement[] = isChildrenArray ? children : [children];
if (!isChildrenArray) {
return (
<S.Component as={as} ref={ref} style={css} {...rest}>
{children}
</S.Component>
);
}
return (
<S.Component as={as} ref={ref} style={css} {...rest}>
{childArray.map((child) => (
<MasonryLayoutItem {...rest}>
{createElement(child.type, child.props)}
</MasonryLayoutItem>
))}
</S.Component>
);
});| export interface _MasonryLayoutProps { | ||
| itemSize: string; |
There was a problem hiding this comment.
itemSize를 px이 아닌 %로 받으면 화면 축소 시에 화면이 작아져도 이미지의 열은 그대로고 이미지 자체가 기괴(?)하게 줄어드는데, Masonry의 경우에는 이미지를 그대로 보존하고 위치가 달라지는 것이 좀 더 낫지 않을까 생각이 듭니다. 혹시 px로 고정값을 받는 것에 대해서는 어떻게 생각하시나요?
There was a problem hiding this comment.
PC에서는 300px, 모바일에서는 200px과 같이 최소 너비를 반응형으로 처리하고 싶었는데요. 이와 비슷하게 %를 사용하면 반응형이 되겠다라고 생각했는데 이것보단, 따로 Props를 받아서 반응형을 구현하는 것이 좋겠다고 생각이 들었습니다
There was a problem hiding this comment.
그래서 클린의 제안대로 px로만 가능하도록 변경하려고 합니다
There was a problem hiding this comment.
itemSize는 px만 받도록 하였고, 화면의 너비마다 다른 px을 줄 수 있도록 다양한 props를 추가해주었습니다
export interface _MasonryLayoutProps {
itemSize: number;
smallItemSize?: number;
mediumItemSize?: number;
largeItemSize?: number;
xLargeItemSize?: number;
doubleXLargeItemSize?: number;
gap?: string;
smallGap?: string;
mediumGap?: string;
largeGap?: string;
xLargeGap?: string;
doubleXLargeGap?: string;
rowGap?: string;
smallRowGap?: string;
mediumRowGap?: string;
largeRowGap?: string;
xLargeRowGap?: string;
doubleXLargeRowGap?: string;
columnGap?: string;
smallColumnGap?: string;
mediumColumnGap?: string;
largeColumnGap?: string;
xLargeColumnGap?: string;
doubleXLargeColumnGap?: string;
}| args: { | ||
| itemSize: '300px', | ||
| fullScreen: true, | ||
| p: '36px', |
There was a problem hiding this comment.
p는 패딩을 뜻하는데요 문서화를 해놓지 않아서 사람들이 헷갈릴 것 같네요 jsDoc과 함께 스토리북에 문서화도 해놓겠습니다 👍
There was a problem hiding this comment.
와 우스 엄청납니다... 상당히 많은 감명을 받았습니다. 저의 모습을 보니 부끄러워 지는군요...
다만, 궁금한 점은 분명히 ReactNode와 ReactElement의 타입이 수행하는 역할이 다른데, 이 부분을 임의로 바꿨을 때 어떤 사이드 이펙트는 없는지 궁금합니다.
We don't just use ComponentType or FunctionComponent types because you are not supposed to attach
statics to this object, but rather to the original function.
위에 있는 주석에 따르면ExoticComponent를 사용하는 다른 컴포넌트들 (memo, context, forwardRef)에서 영향이 없을지 의문이긴 합니다. 실제 주석에 달려있는 코드의 내용을 미루어서 보면 리액트팀에서도 해당 값을 일반적인 컴포넌트 (ReactElement)로 반환하고 싶지만, 정적인 값을 넘기는 방법이 권장되지 않는다는 내용인데, 혹시 어떻게 생각하시나요?? (제 해석이 틀렸을 수도 있습니다)
There was a problem hiding this comment.
@types/react 17버전에서는 ReactElement | null로 되어 있는데요
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/v17/index.d.ts
@types/react 18버전에서는 ReactNode로 바뀌었네요! ㅠㅠ
아래의 PR에서 머지가 되었는데요
DefinitelyTyped/DefinitelyTyped@443451c
해당 PR의 이슈를 보면 이러이러한 내용으로 바꿨다고 하는데 이해는 못했습니다
DefinitelyTyped/DefinitelyTyped#65135
참고: 이 변경 사항은 TypeScript 5.1 이상에만 적용됩니다
TypeScript 5.1에서 요소 유형이 올바른지 확인하기 위해 사용하는 JSX 네임스페이스
아래에 새 ElementType을 추가합니다. 이렇게 하면 함수 구성 요소, forwardRef 구성
요소 등이 Render에서 ReactNode( 문자열, 배열, 숫자, 부울, 정의되지 않음)를 반환할
수 있습니다. 클래스 구성 요소의 경우 이미 의도한 대로 작동했기 때문에 아무것도
변경되지 않습니다.
하지만 안되었던 이유는 찾았어요!
Vite로 설치한 배포할 때 사용하는 프로젝트에서는 최신의 @types/react를 사용하여서 "@types/react": "^18.2.15", 환경에서는 ReactNode | null 로 했을 때 잘 되어서 에러가 나지 않았는데요
막상 배포하고 실험하기 위해 사용했던 react-lunch 프로젝트에서는 에러가 났었어요. 점심뭐먹지 리엑트 미션이니깐 4~5월인 것 같은데요 위의 PR 머지가 된 기간이 4개월 전이니 해당 폴더에서는 ExoticComponent의 타입이 ReactElement | null로 되어 있어서 배포가 잘못되었다고 생각이 들었었던 것이였네요!
방금 최신의 vite 환경에서 제 패키지를 설치하고 ReactNode로 바꾸어서 사용하니 잘 작동이 되는 것을 확인했어요
ReactNode로 바꾼 코드 이미지
빨간 줄이 안보이는 것을 확인할 수 있는 이미지
그리고 현재 최신 버전에서 ExoticComponent의 타입이 ReactNode인데 패키지 컴포넌트 반환 타입을 ReactElement | null을 하여도 에러가 나지 않는 이유는 ReactNode에 ReactElement가 포함되기 때문에 에러가 나지 않는 것을 확인할 수 있었습니다
클린 덕분에 왜 배포하는 곳에서는 에러가 안나고, 배포한 곳에서 에러가 나는지 명확히 이유를 알 수 있었습니다
감사해요
🌊🌊🌊
Gilpop8663
left a comment
There was a problem hiding this comment.
안녕하세요 클린 코드 리뷰 해주셔서 감사합니다 ㅎㅎ
변경된 점으로는
- 다양한 너비에서 반응형이 되도록 함
- resize할 때 너무 실시간으로 height 값이 반응하다보니 버벅이는 것처럼 보였는데요. 이를 방지하고자 디바운스를 적용했습니다
| ...{ | ||
| ...child.props, | ||
| }, |
There was a problem hiding this comment.
디테일하게 봐주셔서 감사합니다 👍
클린이 제안해준 코드로 고쳐볼 수 있을 것 같아요 💯
| args: { | ||
| itemSize: '300px', | ||
| fullScreen: true, | ||
| p: '36px', |
There was a problem hiding this comment.
p는 패딩을 뜻하는데요 문서화를 해놓지 않아서 사람들이 헷갈릴 것 같네요 jsDoc과 함께 스토리북에 문서화도 해놓겠습니다 👍
| export interface _MasonryLayoutProps { | ||
| itemSize: string; |
There was a problem hiding this comment.
PC에서는 300px, 모바일에서는 200px과 같이 최소 너비를 반응형으로 처리하고 싶었는데요. 이와 비슷하게 %를 사용하면 반응형이 되겠다라고 생각했는데 이것보단, 따로 Props를 받아서 반응형을 구현하는 것이 좋겠다고 생각이 들었습니다
| export interface _MasonryLayoutProps { | ||
| itemSize: string; |
There was a problem hiding this comment.
그래서 클린의 제안대로 px로만 가능하도록 변경하려고 합니다
| { css, as, children, ...rest }: MasonryLayoutProps<T>, | ||
| ref: PolymorphicRef<T> | ||
| ) { | ||
| const childArray = Array.isArray(children) ? children : [children]; |
| { css, as, children, ...rest }: MasonryLayoutProps<T>, | ||
| ref: PolymorphicRef<T> | ||
| ) { | ||
| const childArray = Array.isArray(children) ? children : [children]; |
| { css, as, children, ...rest }: MasonryLayoutProps<T>, | ||
| ref: PolymorphicRef<T> | ||
| ) { | ||
| const childArray = Array.isArray(children) ? children : [children]; |
There was a problem hiding this comment.
수정한 코드입니다
수정 전
onst MasonryLayout: MasonryLayoutComponent = forwardRef(function MasonryLayout<
T extends ElementType = 'div'
>(
{ css, as, children, ...rest }: MasonryLayoutProps<T>,
ref: PolymorphicRef<T>
) {
const childArray = Array.isArray(children) ? children : [children];
return (
<S.Component as={as} ref={ref} style={css} {...rest}>
{childArray.map((child) => (
<MasonryLayoutItem {...rest}>
{createElement(child.type, child.props)}
</MasonryLayoutItem>
))}
</S.Component>
);
});수정 후
const MasonryLayout: MasonryLayoutComponent = forwardRef(function MasonryLayout<
T extends ElementType = 'div'
>(
{ css, as, children, ...rest }: MasonryLayoutProps<T>,
ref: PolymorphicRef<T>
) {
const isChildrenArray = Array.isArray(children);
const childArray: ReactElement[] = isChildrenArray ? children : [children];
if (!isChildrenArray) {
return (
<S.Component as={as} ref={ref} style={css} {...rest}>
{children}
</S.Component>
);
}
return (
<S.Component as={as} ref={ref} style={css} {...rest}>
{childArray.map((child) => (
<MasonryLayoutItem {...rest}>
{createElement(child.type, child.props)}
</MasonryLayoutItem>
))}
</S.Component>
);
});| export interface _MasonryLayoutProps { | ||
| itemSize: string; |
There was a problem hiding this comment.
itemSize는 px만 받도록 하였고, 화면의 너비마다 다른 px을 줄 수 있도록 다양한 props를 추가해주었습니다
export interface _MasonryLayoutProps {
itemSize: number;
smallItemSize?: number;
mediumItemSize?: number;
largeItemSize?: number;
xLargeItemSize?: number;
doubleXLargeItemSize?: number;
gap?: string;
smallGap?: string;
mediumGap?: string;
largeGap?: string;
xLargeGap?: string;
doubleXLargeGap?: string;
rowGap?: string;
smallRowGap?: string;
mediumRowGap?: string;
largeRowGap?: string;
xLargeRowGap?: string;
doubleXLargeRowGap?: string;
columnGap?: string;
smallColumnGap?: string;
mediumColumnGap?: string;
largeColumnGap?: string;
xLargeColumnGap?: string;
doubleXLargeColumnGap?: string;
}There was a problem hiding this comment.
@types/react 17버전에서는 ReactElement | null로 되어 있는데요
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/v17/index.d.ts
@types/react 18버전에서는 ReactNode로 바뀌었네요! ㅠㅠ
아래의 PR에서 머지가 되었는데요
DefinitelyTyped/DefinitelyTyped@443451c
해당 PR의 이슈를 보면 이러이러한 내용으로 바꿨다고 하는데 이해는 못했습니다
DefinitelyTyped/DefinitelyTyped#65135
참고: 이 변경 사항은 TypeScript 5.1 이상에만 적용됩니다
TypeScript 5.1에서 요소 유형이 올바른지 확인하기 위해 사용하는 JSX 네임스페이스
아래에 새 ElementType을 추가합니다. 이렇게 하면 함수 구성 요소, forwardRef 구성
요소 등이 Render에서 ReactNode( 문자열, 배열, 숫자, 부울, 정의되지 않음)를 반환할
수 있습니다. 클래스 구성 요소의 경우 이미 의도한 대로 작동했기 때문에 아무것도
변경되지 않습니다.
하지만 안되었던 이유는 찾았어요!
Vite로 설치한 배포할 때 사용하는 프로젝트에서는 최신의 @types/react를 사용하여서 "@types/react": "^18.2.15", 환경에서는 ReactNode | null 로 했을 때 잘 되어서 에러가 나지 않았는데요
막상 배포하고 실험하기 위해 사용했던 react-lunch 프로젝트에서는 에러가 났었어요. 점심뭐먹지 리엑트 미션이니깐 4~5월인 것 같은데요 위의 PR 머지가 된 기간이 4개월 전이니 해당 폴더에서는 ExoticComponent의 타입이 ReactElement | null로 되어 있어서 배포가 잘못되었다고 생각이 들었었던 것이였네요!
방금 최신의 vite 환경에서 제 패키지를 설치하고 ReactNode로 바꾸어서 사용하니 잘 작동이 되는 것을 확인했어요
ReactNode로 바꾼 코드 이미지
빨간 줄이 안보이는 것을 확인할 수 있는 이미지
그리고 현재 최신 버전에서 ExoticComponent의 타입이 ReactNode인데 패키지 컴포넌트 반환 타입을 ReactElement | null을 하여도 에러가 나지 않는 이유는 ReactNode에 ReactElement가 포함되기 때문에 에러가 나지 않는 것을 확인할 수 있었습니다
클린 덕분에 왜 배포하는 곳에서는 에러가 안나고, 배포한 곳에서 에러가 나는지 명확히 이유를 알 수 있었습니다
감사해요
🌊🌊🌊
hozzijeong
left a comment
There was a problem hiding this comment.
안녕하세요 우스~
리뷰가 많이 늦었습니다 (죄송합니다 ㅠ)
바뀐 코드 잘 봤습니다!! 실제로 작동시켜보니 너무 잘 되더라구요~
그럼 이제 그만 다음 단계로 넘어가시죠!! 고생 많이하셨습니다!
| const debounce = (func: any, timeout = 300) => { | ||
| let timer: NodeJS.Timeout; | ||
|
|
||
| return (...args: any[]) => { | ||
| clearTimeout(timer); | ||
| timer = setTimeout(() => { | ||
| func.apply(this, args); | ||
| }, timeout); | ||
| }; | ||
| }; |
|
|
||
| @media (min-width: 640px) { | ||
| grid-template-columns: ${({ smallItemSize }) => | ||
| getGridTemplate(smallItemSize)}; | ||
| grid-column-gap: ${({ smallGap, smallColumnGap }) => | ||
| getGridColumGap({ | ||
| responsiveGap: smallGap, | ||
| responsiveColumnGap: smallColumnGap, | ||
| })}; | ||
| } | ||
|
|
||
| @media (min-width: 768px) { | ||
| grid-template-columns: ${({ mediumItemSize }) => | ||
| getGridTemplate(mediumItemSize)}; | ||
| grid-column-gap: ${({ mediumGap, mediumColumnGap }) => | ||
| getGridColumGap({ | ||
| responsiveGap: mediumGap, | ||
| responsiveColumnGap: mediumColumnGap, | ||
| })}; | ||
| } | ||
|
|
||
| @media (min-width: 1024px) { | ||
| grid-template-columns: ${({ largeItemSize }) => | ||
| getGridTemplate(largeItemSize)}; | ||
| grid-column-gap: ${({ largeGap, largeColumnGap }) => | ||
| getGridColumGap({ | ||
| responsiveGap: largeGap, | ||
| responsiveColumnGap: largeColumnGap, | ||
| })}; | ||
| } | ||
|
|
||
| @media (min-width: 1280px) { | ||
| grid-template-columns: ${({ xLargeItemSize }) => | ||
| getGridTemplate(xLargeItemSize)}; | ||
| grid-column-gap: ${({ xLargeGap, xLargeColumnGap }) => | ||
| getGridColumGap({ | ||
| responsiveGap: xLargeGap, | ||
| responsiveColumnGap: xLargeColumnGap, | ||
| })}; | ||
| } | ||
|
|
||
| @media (min-width: 1536px) { | ||
| grid-template-columns: ${({ doubleXLargeItemSize }) => | ||
| getGridTemplate(doubleXLargeItemSize)}; | ||
| grid-column-gap: ${({ doubleXLargeGap, doubleXLargeColumnGap }) => | ||
| getGridColumGap({ | ||
| responsiveGap: doubleXLargeGap, | ||
| responsiveColumnGap: doubleXLargeColumnGap, | ||
| })}; | ||
| } |




안녕하세요 클린~!
npm 패키지 사이트
https://www.npmjs.com/package/wus-layout-component
스토리북 문서
https://gilpop8663.github.io/layout-component/?path=/docs/intro--docs
설치 방법
npm i wus-layout-component styled-components -D🎯 2단계. 고급 레이아웃 컴포넌트
공통
컴포넌트 요구사항
📌 구현한 내용 설명
MasonryLayout 컴포넌트
의도하였던 사용자는 핀터레스트, 언스플레시와 같은 화면을 구현하는 개발자로 타겟을 잡았습니다.
처음에는 flex, flex와 같이 해야하나 싶었는데 구현 방법이 떠오르지 않아서 grid로 구현하였습니다. 오늘 오전에 참새의 컴포넌트를 경험해보았는데 많은 영향을 받았습니다. grid로 구현하니 flex를 이용하는 방법에 비해 유연성이 엄청나게 높아진 것을 느꼈어요.
ResizeObserver를 처음 사용해보았는데요
'ResizeObserver'는 요소의 콘텐츠 사각형이 변경될 때 알려줍니다. 그에 따라 대응할 수 있도록 크기를 조정합니다.와 같은 설명을 할 수 있고요. 아직은 제대로 이해하진 못한 것 같아서 다른 경우에서도 사용해보고 싶은 마음이 듭니다.그리고 스텝1에서 타입 에러가 나던 부분을 해결해보았는데요. types@react의 타입을 변경하는 형식으로 해결했고, docs 파일에 정리해두었습니다.
MasonryLayout는 아래와 같은 기능들을 고려해서 Props를 설정했습니다
패키지 설치 후 자동 완성이 안되는데 얼른 고쳐보도록 하겠습니다. 근데 너무 고통스러워요
📸 스크린샷 (선택 사항)
https://gilpop8663.github.io/layout-component/?path=/docs/masonrylayout--docs
🔗 참고 링크 (선택 사항)
https://wit.nts-corp.com/2022/10/26/6595
https://medium.com/@andybarefoot/a-masonry-style-layout-using-css-grid-8c663d355ebb
https://ellen92.tistory.com/64
https://w3bits.com/css-grid-masonry/
https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
https://github.com/woowacourse/layout-component/pull/58/files#diff-914ac3af5acb7507102327d7a0965b7fd543635b8addd127329ef953577f70d3 (참새 PR)