카테고리 없음
리액트, css 유즈케이스 이것저것
잘먹는 개발자 에단
2025. 6. 27. 16:37
가로 크기에 따라서 반응형으로 나열하려면
<div
css={css`
display : grid;
grid-template-columns : repeat(auto-fill, minmax(350px, 1fr);
gap : 20px;
`}>
1. repeat(auto-fill, minmax(350px, 1fr))
- auto-fill : 가능한 많은 열을 자동으로 생성한다.
- minmax(350px, 1fr) : 최소 350px, 최대 1fr (남은 공간을 자동분배한다 )
- 결과 : 화면 폭에 따라서 자동으로 열 개수를 조정한다.
카드나 어떤 요소에 호버 줄 때
css={css`
transition : all 0.2s ease;
&:hover{
transform : translateY(-2px);
box-shadow : 0 4px 16px rgba (0,0,0,0.08);
}
`}
검색/필터링 디바운스 구현
useEffect(()=>{
const debounceTimer = setTimeout(()=>{
loadAnnouncements();
},300);
retur () => clearTimeout(debounceTimer);
}
,[searchTerm, filterTop])
1. 성능 최적화 핵심
- 사용자가 타이핑할 때마다 api를 호출하지 않는다.
- 300ms 후에만 검색을 실행한다.
- 이전 타이머는 취소하여 중복 호출을 방지한다.
조건부 스타일링 패턴
{announcement.istop === "1" && (
<Pin size={16} css={css`color: ${colors.warning};`} />
)}
{announcement.ishidden === 1 && (
<span css={css`
background: ${colors.error}15;
color: ${colors.error};
`}>
숨김
</span>
)}
모달을 구현해보자
<Modal
title = {modalMode === "create" ? "새 공지사항 작성":"공지사항 수정}
open = {showModal}
onOk = {handleSave}
onCancel = {closeModal}
width={600}
>
만약에 실제로 구현한다면
<div
css={css`
/* 오버레이 - 전체 화면을 덮는 배경 */
position : fixed;
top : 0;
left : 0;
width : 100vw;
height : 100vh;
background : 색 아무거나
backdrop-filter : blur(4px); /* 뒷배경 블러 처리 */
/* 중앙 정렬을 위한 flexbox */
display : flex;
align-items : center;
justify-content : center;
/* 부드러운 등장 애니메이션 */
animation : fadeIn 0.2s ease-out;
@keyframes fadeIn{
from {opacity : 0; }
to{ opacity : 1; }
}
`}
onClick={onClose} // 오버레이 클릭 시에 닫기
>
{/* 실제 모달 컨텐츠 */}
<div css={css`
background: 색깔;
.... 이것저것 속성들
min-width : 400px;
max-width : 90vw;
max-height : 90vh;
box-shadow : 0 20px 60px rgba(0,0,0,0.3);
/* 모달 등장 애니메이션 */
animation : slideUp 0.3s ease-out;
@keyframes slideUp{
from{
opacity : 0;
transform : translateY(20px) scale(0.95);
}
to{
opacity : 1;
transform : translateY(0) scale(1)
}
}
`}
onClick={(e) => e.stopPropagation()} // 모달 내부 클릭시 닫히지 않도록
>
{children}
</div>
</div>
);
};
display : grid 심화
auto-fill vs auto-fit
// 현재 사용 중인 방식
css={css`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
`}
auto-fill
화면 1200px, 아이템 2개 → [아이템][아이템][빈공간][빈공간]
화면 800px, 아이템 2개 → [아이템][아이템]
auto-fit으로 바꾸면
화면 1200px, 아이템 2개 → [아이템 ][아이템 ] (자동 확장)
화면 800px, 아이템 2개 → [아이템][아이템]
성능 최적화 패턴
1. 메모이제이션과 가상화
// useMemo로 비싼 연산을 캐싱한다.
const filterAnnouncements = useMemo(()=>{
return announcements.filter(item =>
item.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
(filterTop === "" || item.istop === filterTop)
);
}
,[announcements, searchTerm, filterTop]);
// useCallback으로 함수 레퍼런스 안정화
const handleSearch = useCallback(
debounce((term : string)=>{
setSearchTerm(term);
},300),
[]
);
const AnnouncementCard = React.memo<{announcement : Announcement}>(()=>{
return(
<div>{ /*카드 내용*/}</div>
)
});
고급 레이아웃 패턴
flexbox + grid 조합
// 패턴1 : 헤더-컨텐츠-푸터 레이아웃
css={css`
display : flex;
flex-direction : column;
min-height : 100vh;
.header{
flex : 0 0 auto; // 크기고정
}
.content{
flex : 1 1 auto; // 남은 공간 모두 차지
display: grid;
grid-template-columns : repeat(auto-fit, minmax(300px, 1fr));
gap : 20px;
padding : 20px;
}
.footer{
flex: 0 0 auto; // 크기고정
}
`}
// 패턴 2: 사이드바 - 메인 레이아웃
css={css`
display : grid;
grid-template-columns : 260px 1fr; // 고정폭 사이드바 + 유동적인 메인화면
grid-template-rows : 100vh;
@media(max-width : 768px){
grid-template-columns : 1fr; // 모바일에서는 사이드바 숨김
}
`}
고급 css 애니메이션 패턴
// 카드 호버 효과 고급버전
css={css`
transition : all 0.3s cubic-bezier(0.4,0,0.2,1);
&:hover{
transform : translateY(-4px) scale(1.02); // 살짝 크게 + 위로
box-shadow:
0 10px 40px rgba(0, 0, 0, 0.15),
0 4px 12px rgba(0, 0, 0, 0.1); /* 이중 그림자 */
/* 내부 요소들도 함께 애니메이션 */
.card-title{
color : ${colors.primary};
}
.card-icon{
transform : rotate(5deg) scale(1.1);
}
/* 활성상태 */
&:active{
transform : translateY(-2px) scale(1.01);
transition-duration : 0.1s;
}
}
`}
// 리스트 아이템 스태거 애니메이션
css={css`
.list-item{
opacity : 0;
transform : translateY(20px);
animation : fadeInUp 0.5s ease-out forwards;
}
.list-item:nth-child(1){animation-delay : 0.1s;}
.list-item:nth-child(2){animation-delay : 0.2s;}
.list-item:nth-child(3){animation-delay : 0.3s;}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
`}
이중 그림자 Double Shadow
1. 단일 그림자 vs 이중 그림자
// 일반적인 단일 그림자 (평면적)
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
// 이중 그림자 (입체적)
box-shadow:
0 10px 40px rgba(0, 0, 0, 0.15), /* 큰 그림자 (전체적인 깊이감) */
0 4px 12px rgba(0, 0, 0, 0.1); /* 작은 그림자 (세밀한 그림자) */
이중 그림자의 구성요소
box-shadow:
/* 첫 번째 그림자 - 전체적인 깊이감 */
0 /* x-offset: 가로 이동 없음 */
10px /* y-offset: 아래로 10px */
40px /* blur-radius: 40px만큼 흐림 */
rgba(0, 0, 0, 0.15), /* 색상: 연한 검은색 (15% 투명도) */
/* 두 번째 그림자 - 세밀한 그림자 */
0 /* x-offset: 가로 이동 없음 */
4px /* y-offset: 아래로 4px */
12px /* blur-radius: 12px만큼 흐림 */
rgba(0, 0, 0, 0.1); /* 색상: 더 연한 검은색 (10% 투명도) */
이중 그림자의 시각적 효과
// Material Design의 실제 그림자 시스템
const shadowLevels = {
level1: `
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24)
`,
level2: `
0 3px 6px rgba(0, 0, 0, 0.16),
0 3px 6px rgba(0, 0, 0, 0.23)
`,
level3: `
0 10px 20px rgba(0, 0, 0, 0.19),
0 6px 6px rgba(0, 0, 0, 0.23)
`,
level4: `
0 14px 28px rgba(0, 0, 0, 0.25),
0 10px 10px rgba(0, 0, 0, 0.22)
`,
level5: `
0 19px 38px rgba(0, 0, 0, 0.30),
0 15px 12px rgba(0, 0, 0, 0.22)
`
};