카테고리 없음

리액트, 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)
  `
};