이슈 배경
사이드 프로젝트 작업을 하면서 FE 시스템을 개편하였다.
React Native를 사용하고 있었고 기존에 View, SafeAreaView, ScrollView를 직접 사용하고 있었는데
공통화된 속성을 쉽게 적용할 수 있는 ScreenContainer, ContentContainer, ScrollContentContainer를 사용해 화면을 그리도록 수정하였다.
ScreenContainer, ContentContainer, ScrollContentContainer 코드는 글 하단에 두었다.
이슈 내용
문제는 ScrollContentContainer를 사용할 때 문제가 발생했다.
스크롤 상황에서 화면 최상단으로 이동하는 Go To Top 버튼에서 ref를 사용하는데
기존에 ScrollView로 사용하던 것이 잘 동작했는데 단순히 ScrollContentContainer로 바꾸자마자 동작하지 않게 되었다.
이슈 해결
이를 해결하기 위해 forwardRef를 활용하였다.
일반적인 방식으로는 부모 컴포넌트에서 자식 컴포넌트로 ref를 전달할 수가 없었다.
forwardRef – React
The library for web and native user interfaces
ko.react.dev
ref가 필요할 때가 있는데 FE 작업할 때 forwardRef는 기본적으로 알아두어야겠다.
Container 관련 코드
type ScreenContainerProps = {
flexDirections?: string;
justifyContent?: string;
alignItems?: string;
gap?: number;
// Border & Shadow
withUpperShadow?: boolean;
withBorder?: boolean;
withDebugBorder?: boolean;
borderRadius?: number;
};
export const ScreenContainer = styled.SafeAreaView<ScreenContainerProps>`
width: 100%;
height: 100%;
display: flex;
background-color: ${Color.WHITE};
gap: ${props => props.gap ?? 16}px;
flex-direction: ${'column'};
justify-content: ${'stretch'};
align-items: ${'center'};
align-content: space-around;
/* Border & Shadow */
${props => props.withUpperShadow && 'box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);'}
${props => props.withBorder && `border: 1px solid ${Color.GRAY};`}
${props => props.withDebugBorder && 'border: 1px solid red;'}
border-radius: ${props => props.borderRadius ?? 0}px;
`;
type ContentContainerProps = {
// Size
width?: string;
height?: string;
minHeight?: string;
// Layout
useHorizontalLayout?: boolean;
flex?: number;
gap?: number;
absoluteTopPosition?: boolean;
absoluteBottomPosition?: boolean;
absoluteLeftPosition?: boolean;
absoluteRightPosition?: boolean;
expandToEnd?: boolean;
// Align
alignCenter?: boolean;
justifyContent?: string;
alignItems?: string;
// Padding
withScreenPadding?: boolean;
withContentPadding?: boolean;
paddingVertical?: number;
paddingHorizontal?: number;
paddingTop?: number;
paddingBottom?: number;
paddingLeft?: number;
paddingRight?: number;
// Border & Shadow
withUpperShadow?: boolean;
withBorder?: boolean;
withDebugBorder?: boolean;
borderRadius?: number;
borderTopRadius?: number;
borderBottomRadius?: number;
// ETC
opacity?: number;
zIndex?: number;
backgroundColor?: string;
withNoBackground?: boolean;
};
export const ContentContainer = styled.View<ContentContainerProps>`
/* Size */
width: ${props => props.width ?? '100%'};
height: ${props => props.height ?? 'auto'};
${props => props.minHeight && `min-height: ${props.minHeight}`}
/* Flex Basic */
display: flex;
${props => props.expandToEnd && 'flex-grow: 1;'}
${props => props.flex && `flex: ${props.flex}`}
/* Layout */
flex-direction: ${props => (props.useHorizontalLayout ? 'row' : 'column')}
justify-content: ${props =>
props.useHorizontalLayout ? 'space-between' : 'flex-start'};
align-items: ${props => (props.useHorizontalLayout ? 'center' : 'stretch')};
gap: ${props => props.gap ?? 16}px;
${props =>
(props.absoluteTopPosition ||
props.absoluteBottomPosition ||
props.absoluteLeftPosition ||
props.absoluteRightPosition) &&
'position: absolute;'}
${props => props.absoluteTopPosition && 'top: 0;'}
${props => props.absoluteBottomPosition && 'bottom: 0;'}
${props => props.absoluteLeftPosition && 'left: 0;'}
${props => props.absoluteRightPosition && 'right: 0;'}
/* Align */
${props =>
props.alignCenter && 'align-items: center; justify-content: center;'}
${props =>
props.justifyContent && `justify-content: ${props.justifyContent};`}
${props => props.alignItems && `align-items: ${props.alignItems};`}
/* Padding */
${props => props.withScreenPadding && 'padding: 16px 20px 16px 20px;'}
${props => props.withContentPadding && 'padding: 16px'}
${props =>
props.paddingVertical !== undefined &&
css`
padding-top: ${props.paddingVertical}px;
padding-bottom: ${props.paddingVertical}px;
`}
${props =>
props.paddingTop !== undefined &&
`padding-top: ${props.paddingTop}px;
`}
${props =>
props.paddingBottom !== undefined &&
css`
padding-bottom: ${props.paddingBottom}px;
`}
${props =>
props.paddingHorizontal !== undefined &&
css`
padding-left: ${props.paddingHorizontal}px;
padding-right: ${props.paddingHorizontal}px;
`}
/* Border & Shadow */
${props =>
props.withUpperShadow &&
Platform.select({
ios: `
shadow-offset: {width: 0, height: -4px}; /* Upper shadow */
shadow-opacity: 0.2; /* Full opacity since rgba already has the alpha */
shadow-radius: 4px; /* Blur radius */
`,
android: `
elevation: 4; /* Android shadow effect */
`,
})}
${props => props.withBorder && `border: 1px solid ${Color.GRAY};`}
${props => props.withDebugBorder && 'border: 1px solid red;'}
border-radius: ${props => props.borderRadius ?? 0}px;
${props =>
props.borderTopRadius &&
`border-top-left-radius: ${props.borderTopRadius}px;`};
${props =>
props.borderTopRadius &&
`border-top-right-radius: ${props.borderTopRadius}px;`};
${props =>
props.borderBottomRadius &&
`border-bottom-left-radius: ${props.borderBottomRadius}px;`};
${props =>
props.borderBottomRadius &&
`border-bottom-right-radius: ${props.borderBottomRadius}px;`};
/* ETC */
background-color: ${props => props.backgroundColor ?? Color.WHITE};
${props => props.withNoBackground && 'background-color: transparent;'}
opacity: ${props => props.opacity ?? 100};
z-index: ${props => props.zIndex ?? 0};
overflow: hidden;
`;
type ScrollContentContainerProps = ContentContainerProps &
RefAttributes<ScrollView> & {
onScroll?:
| ((event: NativeSyntheticEvent<NativeScrollEvent>) => void)
| undefined;
children?: ReactNode;
};
export const ScrollContentContainer = forwardRef(
(props: ScrollContentContainerProps, ref: React.LegacyRef<ScrollView>) => (
<ScrollView
ref={ref}
onScroll={props.onScroll}
scrollEventThrottle={100}
style={{width: '100%'}}
horizontal={props.useHorizontalLayout}
showsVerticalScrollIndicator={false}>
<ContentContainer {...(props as ContentContainerProps)}>
{props.children}
</ContentContainer>
</ScrollView>
),
);
'트러블슈팅' 카테고리의 다른 글
ObjectMapper의 FAIL_ON_UNKNOWN_PROPERTIES 설정 (0) | 2024.11.16 |
---|---|
테스트가 간헐적으로 실패한다면 비동기 로직을 점검해보자 (0) | 2024.09.01 |
MariaDB Connector/J 드라이버 readOnly가 master로 가는 현상 (0) | 2024.06.29 |
@EnableAsync를 놓치지 말자.. (1) | 2024.06.23 |
AWS EB CName Swap 이후 트래픽이 빠지는데 40분이나 걸린 이유는 아마도 Keep-Alive? (0) | 2024.06.01 |