🎨Frontend

채팅 목록 로딩 느림 SSR 적용으로 해결하기

채팅 목록 로딩이 느려 사용자 경험을 해치던 문제를 SSR 적용으로 해결했습니다. Nginx 설정 변경과 프론트엔드 코드 수정을 통해 초기 API 요청을 줄여 속도를 개선했습니다.

📅 2026년 8월 4일·📖 19분 읽기·👁 3

채팅 목록 로딩 느림 SSR 적용으로 해결하기

채팅 목록 로딩이 느려서 사용자 경험을 해치고 있었다. 특히 초기 마운트 시 불필요한 API 요청이 왕복으로 발생해서 더 답답하게 느껴졌다. 이 문제를 해결하기 위해 Nginx 설정 변경과 Server-Side Rendering(SSR) 적용을 시도해 봤다.

시도와 함정

처음에는 Nginx 설정을 변경해서 HTTP/2를 활성화하는 것부터 시작했다. 이게 네트워크 통신을 더 효율적으로 만들어 줄 거라고 생각했다.

# nginx_v2.conf

http { # ... 다른 설정들 ...

server {
    listen 80;
    listen [::]:80;
    server_name example.com;

    # HTTP/2 활성화
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    # ... SSL 설정 ...

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # ... 다른 설정들 ...
}

}

HTTP/2 설정 자체는 어렵지 않았는데, 이걸로 채팅 목록 로딩 속도가 눈에 띄게 개선되지는 않았다. 문제는 여전히 남아있었고, 초기 마운트 시 발생하는 API 요청을 줄여야 했다. 그래서 대화 목록을 Server-Side Rendering(SSR)으로 HTML과 함께 전송하도록 프론트엔드 코드를 수정하기로 했다.

PageClient.tsx 파일에서 useConversations 훅을 호출하여 데이터를 가져오는 부분을 SSR 친화적으로 변경했다.

// PageClient.tsx

'use client';

import { useEffect, useState } from 'react'; import { useConversations } from './useConversations'; // useConversations 훅 임포트

interface Conversation { id: string; name: string; lastMessage: string; }

export default function PageClient() { // SSR 환경에서는 서버에서 데이터를 미리 받아오므로, // 클라이언트에서는 초기 데이터가 이미 존재한다고 가정하거나, // 서버에서 전달된 데이터를 사용하도록 처리해야 함. // 여기서는 useConversations 훅이 SSR 데이터를 처리한다고 가정.

const { conversations, isLoading, error } = useConversations();

if (isLoading) {
    return <div>Loading conversations...</div>;
}

if (error) {
    return <div>Error loading conversations: {error.message}</div>;
}

return (
    <div>
        <h1>Conversations</h1>
        {conversations.length === 0 ? (
            <p>No conversations found.</p>
        ) : (
            <ul>
                {conversations.map((conv: Conversation) => (
                    <li key={conv.id}>
                        <h2>{conv.name}</h2>
                        <p>{conv.lastMessage}</p>
                    </li>
                ))}
            </ul>
        )}
    </div>
);

}

chat/page.tsx 에서는 PageClient 컴포넌트를 렌더링하도록 했다.

// chat/page.tsx

import PageClient from './PageClient';

export default function ChatPage() {
    // 이 페이지는 클라이언트 컴포넌트인 PageClient를 렌더링합니다.
    // 서버 사이드 렌더링(SSR) 로직은 PageClient 또는 그 안의 훅에서 처리됩니다.
    return <PageClient />;
}

그리고 핵심 로직인 useConversations.ts 훅을 수정했다. SSR 환경에서 서버에서 데이터를 미리 가져오도록 로직을 추가했다.

// useConversations.ts

import { useState, useEffect } from 'react';

interface Conversation { id: string; name: string; lastMessage: string; }

// 이 훅은 서버와 클라이언트 양쪽에서 실행될 수 있습니다. // SSR 환경에서는 서버에서 데이터를 미리 가져와서 초기 상태를 설정합니다. export function useConversations() { const [conversations, setConversations] = useState<Conversation[]>([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<Error | null>(null);

useEffect(() =&gt; {
    const fetchConversations = async () =&gt; {
        try {
            // 서버 사이드 렌더링 시에는 이미 데이터가 준비되었을 수 있습니다.
            // Next.js의 경우, 서버 컴포넌트에서 데이터를 미리 가져와 props로 전달하거나,
            // 클라이언트 컴포넌트에서도 동일한 데이터 fetching 로직을 실행하되,
            // 캐싱 등을 통해 중복 요청을 방지해야 합니다.
            // 여기서는 간단하게 API 호출을 가정합니다.
            // 실제 SSR 구현에서는 서버에서 fetch된 데이터를 초기 state로 설정하는 방식이 일반적입니다.

            // 예시: 서버에서 fetch된 데이터를 초기 state로 설정하는 로직 (Next.js App Router 가정)
            // const initialData = await getServerSideConversations(); // 서버에서 미리 가져온 데이터
            // setConversations(initialData);
            // setIsLoading(false);

            // 클라이언트 측에서만 실행되는 경우
            if (typeof window !== 'undefined') {
                const response = await fetch('/api/conversations'); // 실제 API 엔드포인트
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                const data = await response.json();
                setConversations(data);
                setIsLoading(false);
            }
        } catch (err: any) {
            setError(err);
            setIsLoading(false);
        }
    };

    // SSR 환경에서는 서버에서 데이터를 미리 로드하므로,
    // 클라이언트에서는 이미 데이터가 있을 수 있습니다.
    // useEffect는 클라이언트에서만 실행되므로, 여기서 fetch를 호출하는 것은
    // 클라이언트 측에서만 필요한 경우입니다.
    // SSR 시에는 서버 컴포넌트나 getStaticProps/getServerSideProps 등에서
    // 데이터를 미리 가져와서 초기 state를 설정하는 것이 더 효율적입니다.
    // 이 예시에서는 클라이언트 측에서의 fetch를 가정합니다.
    fetchConversations();

}, []); // 빈 배열은 컴포넌트 마운트 시 한 번만 실행됨을 의미

return { conversations, isLoading, error };

}

원인

Nginx에서 HTTP/2를 활성화한 것은 좋았지만, 근본적인 문제는 초기 마운트 시 발생하는 클라이언트 측의 API 요청이었다. HTTP/1.1에서는 요청 하나당 하나의 연결을 사용하기 때문에, 여러 API 요청이 순차적으로 실행되면서 로딩 지연이 발생했다. SSR을 적용하여 HTML과 함께 데이터를 미리 전송함으로써, 클라이언트 측에서 별도의 API 호출 없이 바로 데이터를 표시할 수 있게 되었다.

해결

nginx_v2.conf 설정으로 HTTP/2를 활성화하고, PageClient.tsxuseConversations.ts의 로직을 수정하여 SSR을 구현했다. 특히 useConversations.ts 훅에서는 서버에서 데이터를 미리 가져와 초기 상태를 설정하도록 변경하여, 클라이언트 측에서의 불필요한 fetch 요청을 제거했다.

# nginx_v2.conf (HTTP/2 활성화 부분)

http { # ...

server {
    listen 443 ssl http2; # HTTP/2 활성화
    listen [::]:443 ssl http2;

    # ... SSL 설정 ...

    location / {
        proxy_pass http://localhost:3000;
        # ... proxy headers ...
    }
}

}

// PageClient.tsx (SSR 로직 적용)

'use client';

import { useConversations } from './useConversations';

interface Conversation {
    id: string;
    name: string;
    lastMessage: string;
}

export default function PageClient({ initialConversations }: { initialConversations?: Conversation[] }) {
    // initialConversations는 서버에서 미리 가져온 데이터 (SSR 시)
    const { conversations, isLoading, error } = useConversations(initialConversations);

    if (isLoading) {
        return <div>Loading conversations...</div>;
    }

    if (error) {
        return <div>Error loading conversations: {error.message}</div>;
    }

    return (
        <div>
            <h1>Conversations</h1>
            {conversations.length === 0 ? (
                <p>No conversations found.</p>
            ) : (
                <ul>
                    {conversations.map((conv: Conversation) => (
                        <li key={conv.id}>
                            <h2>{conv.name}</h2>
                            <p>{conv.lastMessage}</p>
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
}
// chat/page.tsx (SSR 데이터 전달)

import PageClient from './PageClient';

// 서버 컴포넌트에서 데이터를 미리 가져옵니다.
async function getInitialConversations() {
    // 실제로는 API 호출 또는 DB 조회 로직이 들어갑니다.
    // 예시:
    const res = await fetch('http://localhost:3000/api/conversations', { next: { revalidate: 60 } });
    if (!res.ok) {
        throw new Error('Failed to fetch initial conversations');
    }
    return res.json();
}

export default async function ChatPage() {
    const initialConversations = await getInitialConversations();
    return <PageClient initialConversations={initialConversations} />;
}
// useConversations.ts (SSR 데이터 처리 로직 추가)

import { useState, useEffect } from 'react';

interface Conversation {
    id: string;
    name: string;
    lastMessage: string;
}

export function useConversations(initialData?: Conversation[]) {
    const [conversations, setConversations] = useState<Conversation[]>(initialData || []);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState<Error | null>(null);

    useEffect(() => {
        const fetchConversations = async () => {
            // SSR 시 initialData가 제공되면 로딩 상태를 바로 false로 설정
            if (initialData) {
                setIsLoading(false);
                return;
            }

            // 클라이언트 측에서만 실행되는 경우
            if (typeof window !== 'undefined') {
                try {
                    const response = await fetch('/api/conversations'); // 실제 API 엔드포인트
                    if (!response.ok) {
                        throw new Error(`HTTP error! status: ${response.status}`);
                    }
                    const data = await response.json();
                    setConversations(data);
                    setIsLoading(false);
                } catch (err: any) {
                    setError(err);
                    setIsLoading(false);
                }
            }
        };

        fetchConversations();

    }, [initialData]); // initialData 변경 시 useEffect 재실행

    return { conversations, isLoading, error };
}

결과

  • 채팅 목록 초기 로딩 시간이 눈에 띄게 단축되었다.
  • 초기 마운트 시 발생하는 불필요한 API 왕복 요청이 제거되었다.
  • 사용자 경험이 전반적으로 개선되었다.

정리 — 같은 함정 안 빠지려면

  • [ ] Nginx 설정에서 HTTP/2를 활성화하여 네트워크 통신 효율을 높였는지 확인한다.
  • [ ] 채팅 목록과 같이 초기 로딩 시 중요한 데이터는 Server-Side Rendering(SSR) 또는 Static Site Generation(SSG)을 적용하여 서버에서 미리 렌더링된 HTML과 함께 전송하도록 한다.
  • [ ] 클라이언트 컴포넌트에서 useEffect 등을 사용하여 API 요청을 할 때는, SSR 시 이미 데이터가 전달되었는지 확인하고 중복 요청을 방지하는 로직을 추가한다.
  • [ ] useConversations와 같은 훅은 서버와 클라이언트 양쪽에서 모두 잘 동작하도록 설계한다.

태그

#SSR#채팅 목록 로딩#성능 개선#Nginx#프론트엔드 최적화#사용자 경험