Skip to content

[4주차] 박유민 과제 제출합니다.#16

Open
waldls wants to merge 94 commits intoCEOS-Developers:masterfrom
waldls:waldls
Open

[4주차] 박유민 과제 제출합니다.#16
waldls wants to merge 94 commits intoCEOS-Developers:masterfrom
waldls:waldls

Conversation

@waldls
Copy link
Copy Markdown

@waldls waldls commented Apr 24, 2026

작업 관련 링크

과제를 하며

느낀 점 및 배운 점

  • 3주차에서 레이아웃과 공수가 많이 드는 부분들을 미리 처리해두어서, 이번 주는 비교적 수월하게 진행할 수 있었습니다. 디자이너님과 협업하면서 초반 소통을 충분히 가져가는 것이 이후 작업 속도에 직결된다는 걸 다시 한번 느꼈고, 좋은 협업일수록 서로의 작업 방식을 이해하고 배려하는 것이 중요하다는 점도 실감했습니다.
  • 생각보다 남은 페이지들 구현이 오래 걸리지 않았고, QA 피드백도 대부분 소규모 추가 개발 수준이어서, 남은 시간을 기능 구현과 최적화에 집중할 수 있었습니다. 최적화를 고민하면서 React.memouseMemo를 단순히 느릴 것 같으면 쓰는 도구가 아니라, 컴포넌트의 렌더링 비용과 props 변경 빈도를 함께 따져보고 적용해야 한다는 것을 체감했습니다. 무분별하게 사용하면 오히려 오버헤드가 될 수 있다는 점도 함께 고려하게 되었습니다.
  • 위 내용의 연장선에서 로직을 짤 때 시간 복잡도를 의식적으로 고민하게 된 것도 이번 과제에서 얻은 수확 중 하나였습니다.연락처 시간순 정렬에서 사용자별 마지막 메시지 전송 시간을 O(n)으로 계산하거나 즐겨찾기 포함 여부를 Set으로 O(1)에 조회하는 식으로, 작은 선택들이 성능에 미치는 영향을 직접 고민해볼 수 있었습니다.
  • zustand persist를 도입하면서 기본 어댑터로는 모든 채팅 데이터가 하나의 키에 통째로 직렬화되는 구조가 부담스럽게 느껴져, 채팅방 ID별로 스토리지 키를 분리하는 커스텀 어댑터를 구현했습니다. 데이터 규모가 커질수록 저장,복원 비용이 선형적으로 증가하는 기본 방식의 한계를 직접 고민해볼 수 있었고, 스토리지 설계도 성능의 일부라는 걸 느꼈습니다.
  • 클라이언트 단에서만 상태를 관리하다 보니 로직이 점점 복잡해졌고, 어느 순간부터는 구조적인 한계가 느껴지기 시작했습니다. 실제 서비스라면 서버에서 자연스럽게 처리됐을 문제들을 클라이언트 로직으로 우회하는 과정에서 서버가 단순한 데이터 저장소가 아니라 비즈니스 로직의 핵심 기반임을 다시 한번 실감했습니다.

구현한 기능 및 고려사항

1. 스플래시

  • 서비스 진입 시 브랜드를 먼저 인지시키기 위해 루트(/) 경로에 스플래시 페이지를 구현했습니다.
  • 다른 메신저들의 말풍선 로고와 달리 비행기 모양인 텔레그램 로고에 착안하여, framer-motion으로 이륙 애니메이션을 구현했습니다. onUpdate 콜백에서 x 좌표를 추적해 비행기가 화면 밖으로 벗어나는 시점에 /chat으로 자동 이동하도록 구현했습니다.
  • useRef(hasNavigated)로 애니메이션 진행 중 navigate가 중복 호출되는 것을 방지했습니다.

2. 채팅 리스트

  • 채팅방 참여자 수(1~4명)에 따라 프로필 이미지 레이아웃이 다르게 렌더링됩니다. 1명은 원형 단일 프로필, 2명은 대각선 배치, 3명은 상단 1개+하단 2개, 4명은 2×2 격자로 구성됩니다.
  • 각 항목에 최신 메시지(이미지 전송 시 "사진을 보냈습니다."로 대체), 타임스탬프, 읽음 체크, 고정 여부를 표시합니다.
  • 고정 채팅방은 항상 최상단에 위치하고, 나머지는 마지막 메시지 타임스탬프 기준 내림차순으로 정렬됩니다. 메시지 전송 시 정렬 순서가 실시간으로 반영됩니다.
  • 전체 미읽음 메시지 수를 네비바 '대화' 아이콘에 표시하며, 99개 이상이면 99+로 노출됩니다.
  • 이름 기반 실시간 채팅방 검색을 지원합니다. 검색어를 포함하는 참여자가 있는 모든 채팅방이 필터링되어 노출됩니다.
  • 스크롤 시 검색창이 가려지는 문제를 보완하기 위해, 스크롤이 발생하면 헤더에 돋보기 아이콘을 노출하고 클릭 시 최상단으로 부드럽게 이동하도록 구현했습니다.
  • 항목 클릭 시 해당 채팅방(/chat/:id)으로 동적 라우팅됩니다.
  • 읽음 상태(회색→초록 체크)는 나(지민)의 관점 기준으로 동작합니다.
    1. '지민' 시점에서 메시지를 보내고 나오면 → '예지'가 아직 읽지 않아 회색 체크 → 채팅방에 재진입해 헤더를 클릭해 '예지' 관점으로 전환하면 읽음 처리되어 → 나갔을 때 지민의 목록에 초록 체크로 표시
    2. '예지'가 메시지를 보내고 나온 상태 → 지민 목록에 미읽음 표시 → 채팅방 진입해 '지민' 관점으로 전환 시 읽음 처리 → 초록 체크로 표시
    3. 단체 채팅방의 경우 동일하나, 참여자 중 한 명이라도 읽으면 초록 체크로 표시됩니다.

3. 채팅방

3주차 과제에 구현하지 않은 부분 위주로 기술합니다.

  • 클립 아이콘 클릭 시 파일 선택 창이 열리고, FileReader로 Data URL로 변환 후 이미지 메시지로 전송됩니다. 이미지 버블도 텍스트 말풍선과 동일한 cornerClass 로직을 공유하여, 시간 그룹의 첫 메시지 여부에 따라 말풍선 꼭짓점이 동일하게 처리됩니다.
  • 헤더의 이름을 클릭하면 채팅방 참여자 전원을 순서대로 순환하며 관점이 전환됩니다. 관점 전환 시 상대방 메시지가 일괄 읽음 처리됩니다.
  • 채팅 내용 및 읽음 상태는 Zustand persist로 새로고침 후에도 유지됩니다. 기본 어댑터 대신 채팅방 ID별로 localStorage 키를 분리하는 커스텀 스토리지 어댑터를 구현해, 전체 상태를 단일 키에 직렬화하는 방식의 비효율을 개선했습니다.
  • 1:1 채팅 구조를 단체방으로 확장했습니다. friendUserIdfriendUserIds 배열로 변경하고, perspective를 문자열 타입에서 실제 사용자 ID로 변경했습니다. 메시지 그룹핑 로직도 type 기준에서 userId 기준으로 수정해 단체방에서도 동일하게 동작하도록 했습니다.

4. 연락처

  • 내 프로필 영역 클릭 시 프로필 페이지(/profile)로 이동합니다.
  • 기본은 마지막 접속 순 정렬이며, 헤더 우측 아이콘 클릭으로 이름 순으로 토글할 수 있습니다.
  • 이름 순 정렬 시 성 기준 인덱스 섹션 헤더가 렌더링되며, 검색 중에는 섹션 헤더가 자동으로 숨겨집니다.
  • 채팅 리스트와 동일하게, 스크롤 시 헤더에 돋보기 아이콘이 나타나고 클릭 시 최상단으로 이동합니다.

5. 프로필

  • '게시물' / '보관된 게시물' 탭을 토글해 이미지를 구분하여 확인할 수 있습니다.
  • 게시물 추가 버튼으로 이미지를 업로드하면 게시물 목록의 맨 앞에 삽입됩니다.
  • 원하는 이미지를 클릭해 즐겨찾기(별) 상태로 토글할 수 있으며, 업로드 이미지와 즐겨찾기 상태 모두 Zustand persist로 새로고침 후에도 유지됩니다.

6. 설정

  • react-spinners의 ScaleLoader를 서비스 primary 색상으로 적용해 준비 중임을 안내합니다. 여러 스피너 중 텔레그램의 분위기와 가장 어울린다고 판단해 선택했습니다.

7. 공통

  • React.memo를 활용해 불필요한 리렌더링을 방지했습니다.
    MessageProfileImageChatListItemContactListItemTextFieldChatTimeMessageDateToggleTap 총 8개 컴포넌트에 적용했습니다.
  • 채팅방, 채팅 리스트, 프로필, 네비바 등 여러 곳에 걸쳐 반복되는 원형 프로필 컴포넌트의 스타일을 PROFILE_VARIANTS 상수에 chatroom, navibar, chatlist_1/2/3, profile_big 등 사용처별 variant로 정의하고 재사용했습니다.
    이미지 파일이 아닌 색상과 이름 첫 글자를 기반으로 렌더링하는 방식으로 직접 구현했습니다.
  • 정의되지 않은 경로 접근 시 PublicLayout을 유지한 채로 404 페이지를 렌더링했습니다.
    react-router-dom의 path: "*" catch-all 라우트를 활용했습니다.

피드백 반영 사항

  • 아이콘 파일들의 네이밍 규칙을 통일했습니다.
  • 접근성을 고려해 font-size 단위를 px → rem으로 변경했습니다.
  • fonts 폴더 위치를 src/assets/ → public/으로 이동했습니다.
  • vite-plugin-svgr의 svgrOptions: { dimensions: false } 설정을 추가해, 사용처에서 className으로 크기를 자유롭게 지정할 수 있도록 했습니다.

Review Questions

1. React Router의 동적 라우팅(Dynamic Routing)이란 무엇이며, 언제 사용하나요?

  • 개념

    • URL 경로에 파라미터를 포함시켜, 하나의 컴포넌트로 여러 페이지를 처리하는 방식
    • URL의 일부를 변수처럼 사용해 컴포넌트를 재사용
  • 핵심 장점

    • 효율적인 코드 관리 : 수많은 상세 페이지를 개별 라우트로 만들 필요 없이 하나로 통합 가능
    • 유지보수 용이 : UI 구조 변경 시 공통 컴포넌트 한 곳만 수정하면 모든 동적 페이지에 반영
    • 데이터 중심 설계 : URL 파라미터를 키값으로 사용하여 API 호출 및 데이터 연동이 직관적임
  • 예시 코드

    • 라우트 설정

        import { Routes, Route } from 'react-router-dom';
        import PostDetailPage from '@/pages/PostDetailPage';
      
        const App = () => {
          return (
            <Routes>
              {/* :id를 통해 동적 경로를 지정 */}
              <Route path="/post/:id" element={<PostDetailPage />} />
            </Routes>
          );
        };
      
        export default App;
    • 실제 컴포넌트

        import { useParams } from 'react-router-dom';
      
        type PostParams = {
          id: string;
        };
      
        const PostDetailPage = () => {
          // useParams에 제네릭만 줘서 깔끔하게 사용
          const { id } = useParams<PostParams>();
      
          return (
            <div>
              <h1>게시글 번호: {id}</h1>
            </div>
          );
        };
      
        export default PostDetailPage;
  • 사용

    • 게시글, 상품, 유저 등의 상세 페이지 구현 시 (/post/1, /product/2)
    • 동일한 UI 구조에서 데이터만 다른 경우
    • RESTful한 URL 설계가 필요할 때 (/users/:id, /orders/:orderId)

2. 네트워크 속도가 느린 환경에서 사용자 경험을 개선하기 위해 사용할 수 있는 UI/UX 디자인 전략과 기술적 최적화 방법은 무엇인가요?

UI/UX 디자인 전략

물리적 속도를 제어할 수 없는 환경에서 체감 대기 시간을 줄여 심리적 안정감을 제공하는 방법이 있음

  • 스켈레톤 UI (Skeleton UI)
    • 데이터 로드 전 실제 콘텐츠의 레이아웃을 미리 보여줌
    • 사용자가 다음 화면을 예측할 수 있어 단순 스피너보다 이탈률을 낮추는 효과가 있음
  • 낙관적 업데이트 (Optimistic UI)
    • 서버 응답 전 화면 상에서 먼저 성공한 것으로 처리함
    • 좋아요, 북마크 등 가벼운 상호작용에서 즉각적인 피드백을 주기 위해 사용함
  • 진행 상태 가시화
    • 로딩이 길어질 경우 프로그레스 바 등을 활용해 현재 상태를 노출함
    • 시스템이 멈추지 않았음을 인지시켜 이탈을 방지함
  • 이미지 placeholder
    • 저화질 이미지나 Blur 처리를 먼저 노출함
    • 시각적 공백을 채워 페이지가 덜 비어 보이게 하는 효과가 있음

기술적 최적화 방법

리소스 크기를 줄이고 로딩 우선순위를 전략적으로 배분하는 방식이 있음

  • 코드 분할 (Code Splitting)
    • React.lazySuspense를 활용
    • 당장 필요한 번들만 로드하여 초기 구동 속도(LCP)를 개선함
  • 데이터 캐싱 (Caching)
    • React Query 등을 사용해 이전 데이터를 메모리에 저장함
    • 재방문 시 로딩 없이 데이터를 즉시 노출함
  • 이미지 최적화
    • WebP, AVIF 등 고효율 포맷을 도입함
    • srcset을 사용하여 디바이스 해상도에 최적화된 리소스만 전송함
  • 지연 로딩 (Lazy Loading)
    • 브라우저 뷰포트에 들어오지 않은 요소의 로딩을 미룸
    • 초기 네트워크 대역폭 점유를 줄이는 데 효과적임
  • 리소스 압축
    • 서버에서 Gzip 또는 Brotli 압축을 적용함
    • 전송되는 파일 크기를 줄여 로딩 시간을 단축함

3. React에서 useState와 useReducer를 활용한 지역 상태 관리와 Context API 및 전역 상태 관리 라이브러리의 차이점을 설명하세요.

useState와 useReducer를 활용한 지역 상태 관리

특정 컴포넌트 내부에서만 유효한 상태를 관리하며, 부모-자식 간에는 Props로 데이터를 전달함

useState

가장 기본적인 상태 관리 훅으로, 컴포넌트 내에서 독립적인 상태 값을 가질 때 사용

const [count, setCount] = useState<number>(0);

// 직접적으로 상태 업데이트
const onIncrease = () => setCount(prev => prev + 1);
  • 특징 : [상태값, 업데이트 함수] 형태의 배열을 반환하며, 비동기적으로 상태를 업데이트함
  • 장점 : 코드가 매우 단순하고 직관적이며, 학습 곡선이 거의 없음. 소규모 컴포넌트 개발 시 생산성이 높음
  • 단점 : 상태 업데이트 로직이 컴포넌트 내부에 섞여 있어, 로직이 복잡해질수록 컴포넌트 코드가 비대해짐

useReducer

상태 업데이트 로직을 컴포넌트 외부로 분리하여, 여러 가지 분기에 따른 복잡한 상태 변화를 처리할 때 사용

// 로직 분리: 상태 변화를 action으로 정의
const [state, dispatch] = useReducer(reducer, { count: 0 });

// 액션 전달로 상태 업데이트
const onIncrease = () => dispatch({ type: 'INCREMENT' });
  • 특징 : dispatch를 통해 action을 발생시키면 reducer 함수가 새로운 상태를 반환하는 구조임
  • 장점 : 상태 관리 로직을 컴포넌트 외부로 분리할 수 있어 유지보수와 테스트가 용이함. 복잡한 상태 변화를 일관성 있게 관리 가능함
  • 단점 : 초기 설정 코드(보일러플레이트)가 많아 단순한 상태 관리에는 오히려 비효율적임

Context API 및 전역 상태 관리 라이브러리의 차이점

여러 컴포넌트가 공유해야 하는 데이터를 관리하며, Props Drilling 문제를 해결함

Context API

종속성 주입(Dependency Injection) 도구에 가까우며, 단계별 Props 전달 없이 데이터를 하위 트리로 쏴주는 방식임

const AuthContext = createContext(null);

// Provider로 하위 트리 전체에 데이터 주입
<AuthContext.Provider value={user}>
  <App />
</AuthContext.Provider>

// 하위 컴포넌트에서 사용
const user = useContext(AuthContext);
  • 특징 : React 내장 기능으로 별도 설치가 필요 없음. 상위 Provider에서 쏜 데이터를 하위 어디서든 useContext로 꺼내 씀
  • 장점 : 설정이 간편하고 가벼움. 테마, 유저 인증 정보 등 변경 빈도가 낮은 데이터 공유에 최적임
  • 단점 : 성능 최적화가 어려움. Context 값이 바뀌면 이를 구독하는 모든 하위 컴포넌트가 불필요하게 리렌더링됨

전역 상태 관리 라이브러리

외부 스토어에 상태를 저장하고, 필요한 컴포넌트만 정밀하게 상태를 구독하여 성능과 로직을 모두 잡는 방식임

Zustand

const useStore = create((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}));

// 사용: 필요한 상태만 선택(Selector)해서 구독
const count = useStore((state) => state.count);
  • 특징 : 특정 프레임워크에 종속되지 않는 가벼운 라이브러리로, 스토어 함수를 직접 호출하는 자유로운 구조임
  • 장점 : 설정이 매우 간결하고 배우기 쉬움. 선택적 구독(Selector) 덕분에 성능 최적화가 자동임
  • 단점 : 상태 업데이트 로직이 커지면 관리가 복잡해질 수 있음

Redux Toolkit

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; },
  },
});

// 사용: useSelector와 useDispatch 사용
const count = useSelector((state) => state.counter.value);
  • 특징 : 데이터 흐름이 엄격한 Flux 패턴(Action → Reducer → Store)을 따르며, 강력한 미들웨어를 지원함
  • 장점 : 개발자 도구(DevTools)가 압도적으로 좋아 대규모 프로젝트의 디버깅에 유리함
  • 단점 : 여전히 코드가 길고 학습 곡선이 높음

Recoil

const countAtom = atom({ key: 'countAtom', default: 0 });

// 사용: useState와 유사한 문법
const [count, setCount] = useRecoilState(countAtom);
  • 특징 : Facebook에서 만든 라이브러리로, Atom(상태 단위)과 Selector(파생 상태) 기반임
  • 장점 : React스러운 문법이라 배우기 쉬움. 동적 로딩(Suspense)과 연동이 잘 됨
  • 단점 : 라이브러리 유지보수 주기가 느리고, 메인테이너의 관리가 다소 불투명함

Jotai

const countAtom = atom(0);

// 사용: 키(Key) 관리 없이 사용
const [count, setCount] = useAtom(countAtom);
  • 특징 : Recoil의 Atom 개념을 계승한 Bottom-up 방식 라이브러리임
  • 장점 : API가 매우 단순하고 번들 사이즈가 작음. 각 Atom에 문자열 키가 필요 없어 관리가 유연함
  • 단점 : 대규모 앱에서 Atom 구조를 체계화하는 설계 능력이 요구됨

waldls added 30 commits March 24, 2026 14:55
waldls added 30 commits April 1, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant