본문 바로가기
TIL with Programmers

[TIL] 11/15 -webkit-box, Ellipsis, $ 접두사, 낙관적 업데이트

by 보먀 2024. 11. 15.
728x90
반응형

1. EllipsisBox 만들기

ElipsisBox 란 일반적으로 텍스트가 지정된 범위를 초과할 때 나머지 텍스트를 생략하고 말줄임표 (...) 를 표시하도록 스타일링된 컨테이너를 의미한다. 

 

1.1. 전체 코드

interface Props {
    children: ReactNode;
    linelimit: number; // 몇 줄까지 보여줄 것인지
}

const EllipsisBox = ({ children, linelimit }: Props) => {

  const [expanded, setExpanded] = useState(false); // 처음엔 확장되지 않음

  return (
    <EllipsisBoxStyle linelimit={linelimit} $expanded={expanded}>
      <p>{children}</p>
      <div className="toggle">
        <Button size='small' scheme='normal' onClick={() => {setExpanded(!expanded)}}>
            {expanded ? '접기' : '펼치기'} { expanded ? <FaAngleUp /> : <FaAngleDown /> }
        </Button>
      </div>
    </EllipsisBoxStyle>
  )
};

 

expanded 라는 state 를 만들어서 접고 펼칠 수 있는 ElipsisBox 를 만들었다.

expanded 가 true 이면 확장된 상태이므로 접기라는 글자가 버튼에 나타나고, expanded 가 false 라면 확장되지 않은 접혀진 상태이므로 펼치기라는 글자가 버튼에 나타나도록 구현했다. 

 

 

1.2. css 코드

interface EllipsisBoxStyleProps {
    linelimit: number;
    $expanded: boolean;
};

const EllipsisBoxStyle = styled.div<EllipsisBoxStyleProps>`
    p {
        overflow: hidden;
        text-overflow: elipsis;
        display: -webkit-box;
        -webkit-line-clamp: ${({ linelimit, $expanded }) => 
            ($expanded ? 'none' : linelimit)};
        -webkit-box-orient: vertical;
        padding: 20px 0 0 0;
        margin: 0;
    }

    .toggle {
        display: flex;
        justify-content: right;
    }
`;

 

 

- 충돌 방지를 위한 $ 접두사

 

ElipsisBoxStyledProps 의 expanded 속성값에는 $ 라는 접두사가 붙어있다. $ 접두사는 styled-components 라이브러리에서 동적 스타일링을 위한 표기법으로, React 와 HTML props 의 충돌을 방지하기 위해 사용된다. 

 

먼저 styled-components 의 동작 방식에 대해 간단히 알아보자. 

styled-components 는 스타일링에 필요한 props 만 내부적으로 사용하고 나머지 props 는 DOM 으로 전달되도록 설계되어 있다. 이때 $ 접두사를 사용하면 해당 속성이 스타일링 목적임을 명확히 할 수 있어 $ 접두사가 붙은 props 는 DOM 에 전달되지 않는다. 만약 $ 를 붙이지 않는다면, React 가 표준 속성이 아닌 값을 자동으로 제거하지 못할 수 있기 때문에 꼭 붙이는 것을 권장한다. 

 

코드로 돌아와서 설명해보자면, expanded 는 HTML 요소의 표준 속성이 아니기 때문에 $ 접두사를 붙여서 styled-components 가 이 속성이 스타일링을 위한 전용 속성임을 알 수 있도록 명시하여주었다.

const ElipsisBoxStyle = styled.div<ElipsisBoxStyleProps>`
    p {
        overflow: hidden;
        text-overflow: elipsis;
        display: -webkit-box;
        -webkit-line-clamp: ${({ linelimit, $expanded }) => 
            ($expanded ? 'none' : linelimit)};
        -webkit-box-orient: vertical;
        padding: 20px 0 0 0;
        margin: 0;
    }

    .toggle {
        display: flex;
        justify-content: right;
    }
`;

 

expanded 가 true 이면 -webkit-line-clamp 를 none 으로 해서 모든 줄이 보이도록 하고,

expanded 가 false 이면 -webkit-line-clamp 를 props 로 들어온 linelimit 값만큼만 보이도록 해주었다. 

-webkit-line-clamp: ${({ linelimit, $expanded }) => ($expanded ? 'none' : linelimit)};

 

 

그 외의 것들을 간략하게 설명해보면,

  • overflow: hidden -> 컨텐츠 크기가 넘어가는 콘텐츠 숨김
  • text-overflow: ellipsis -> 숨겨진 텍스트 부분에 ... (말줄임표) 표시 (참고로 display: -webkit-box 및 -webkit-line-clamp 와 함께 사용해야 동작함)
  • display: -webkit-box -> CSS Flex 를 기반으로 하지만 텍스트 클리핑(자르기)을 위해 특별히 설계된 
  • -webkit-line-clamp -> 텍스트를 특정 줄 수로 제한하는 속성
  • -webkit-box-orient: 방향을 세로로

 

 

2. 낙관적 업데이트 (Optimistic Updates)

 

낙관적 업데이트는 사용자 경험을 향상시키기 위해 클라이언트 측에서 서버에 요청을 보내기 전에 UI 를 미리 업데이트하는 전략을 말한다. 이 전략을 사용하면 사용자가 느끼는 지연 시간을 줄이고, 애플리케이션을 보다 빠르게 보이게 만든다. 만약 서버 요청이 실패할 경우, 클라이언트는 이전 상태로 롤백하거나 오류를 처리하여 사용자에게 알리는 방식으로 동작한다. 

 

정리하자면, 이런 식으로 동작함.

1. 사용자가 버튼을 클릭하거나 데이터를 입력하는 등 어떤 행동을 함

2. UI 를 즉시 변경 (서버에 요청 안 보낸 상태)

3. 서버에 요청 보냄

4. 서버가 응답 처리 (성공시 추가 작업 X / 실패시 이전 상태로 롤백하거나 오류 메세지 표시)

 

 

2.1. 낙관적 업데이트의 장단점

 

- 장점

  • 향상된 사용자 경험 -> UI 즉시 반영, 사용자에게 빠른 피드백 제공, 지연시간으로 인한 불편함 줄임
  • 성능 향상 -> 네트워크 요청의 지연시간을 숨길 수 있음
  • 사용자 만족도 증가 -> 빠르고 반응적인 인터페이스로 사용자 만족도를 높임

- 단점

  • 서버 요청이 실패하면 클라이언트와 서버 간의 데이터가 일치하지 않을 수 있음
  • 실패시 추가적인 오류 처리 필요 -> UI 원상 복구, 사용자에게 오류 알리기 등
  • 복잡성 증가 -> 상태관리와 오류처리가 복잡해질 수 있음

 

2.3. 낙관적 업데이트 사용해보기 (feat. 좋아요 버튼)

 

코드를 보면 DB 에서 정보를 업데이트 시키고, 업데이트가 되기 전에 setBook 함수를 통해서 상태를 변경하고 있는 것을 확인할 수 있다. (렌더링을 하기 위해)


// DB에 있는 like 정보를 수정하는 함수들
export const likeBook = async (bookId: number) => {
    const response = await httpClient.post(`/likes/${bookId}`);
    return response.data;
};

export const unlikeBook = async (bookId: number) => {
    const response = await httpClient.delete(`/likes/${bookId}`);
    return response.data;
}

// ...

const likeToggle = () => {
        
        if (!isLoggedIn) {
            showAlert('로그인이 필요합니다.');
            return; 
        }

        if (!book) return;

        if (book.liked) {
            // 라이크 상태 -> 언라이크
            unlikeBook(book.id).then(() => {
                setBook({...book, liked: false, likes: book.likes-1});
            })
        } else { 
            // 언라이크 상태 -> 라이크
            likeBook(book.id).then(() => {
                setBook({...book, liked: true, likes: book.likes+1}); 
            })
        }
    };
728x90
반응형