[TAROYAKI] Ep5-2. 대화 세션 유지 및 선택 (Frontend)

Yihoon·2025년 3월 20일

TAROYAKI

목록 보기
7/20
post-thumbnail

내용이 길어지는 관계로 프론트 코드 부분만 따로 분리했다.

fetchsessions

REST API로 백엔드의 taortchat_getsession Lambda와 연결, 세션 목록을 불러오는 함수.

async function fetchSessions(userId) {
	// 사용자 ID가 존재하지 않을 경우 동작하지 않음
    if (!userId) {
        console.error('User ID is required to fetch sessions');
        return;
    }

API 접속 전 토큰 존재를 확인하고 그 유효성을 체크. ensureValidToken 함수를 비롯한 인증 관련 내용은 뒤에서 따로 다루겠다.

    try {
        const token = localStorage.getItem('auth_token');
        if (!token) {
            console.error('No auth token available');
            return;
        }

        await ensureValidToken();

백엔드에서 세션 목록에 대한 응답을 받아온다.

        const response = await fetch(`${config.restEndpoint}/sessions?userId=${userId}`, {
            method: 'GET',
            headers: {
                'Authorization': localStorage.getItem('auth_token'),
                'Content-Type': 'application/json'
            }
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

응답을 파싱하여 세션 목록을 반환하고 표시한다. displaysession 함수는 아래에서 설명하겠다.

        const sessions = await response.json();
        displaySessions(sessions);
        return sessions;
    } catch (error) {
        console.error('Error fetching sessions:', error);
        hideSessionLoadingIndicator();
        if (error.message.includes('token')) {
            handleLogout();
        }
    }
}

displaysession

function displaySessions(sessions) {
    const sessionList = document.getElementById('sessionList');
    if (!sessionList) return; // sessionList 존재하지 않으면 함수 종료

세션 목록 전체에 클릭 이벤트 리스너를 추가하고 기존 세션 목록을 비운다. 이때 hasEventListener 플래그는 이벤트 리스너가 중복 등록되지 않도록 하기 위해 추가하였다.

    if (!sessionList.hasEventListener) {
        sessionList.addEventListener('click', handleSessionClick);
        sessionList.hasEventListener = true;
    }
    
    sessionList.innerHTML = '';

각 세션을 DOM 요소 sessionElement로 생성하였다. 이때 해당 요소가 버튼으로 기능하도록 role="button" 속성을, tab버튼으로 액세스 가능하도록 tabindex="0" 속성을 명시하였다.

    sessions.forEach(session => {
        const sessionElement = document.createElement('div');
        sessionElement.className = 'session-item';
        sessionElement.setAttribute('data-session-id', session.SessionId);
        sessionElement.setAttribute('role', 'button');
        sessionElement.setAttribute('tabindex', '0');

세션명을 표시할 요소와 삭제 버튼을 생성하여 sessionElement에 하위 요소로 삽입한다.

        const sessionName = document.createElement('span');
        sessionName.textContent = session.SessionName;
        sessionName.className = 'session-name';
        
        const deleteButton = document.createElement('button');
        deleteButton.className = 'delete-button';
        deleteButton.setAttribute('data-action', 'delete');
        deleteButton.setAttribute('aria-label', 'Delete session');
        
        sessionElement.appendChild(sessionName);
        sessionElement.appendChild(deleteButton);

세션 ID가 현재 세션 ID와 동일한 경우, 즉 활성화된 세션의 경우 active 클래스를 달아서 현재 활성화된 세션임을 알 수 있도록 한다.

        
        if (session.SessionId === currentSessionId) {
            sessionElement.classList.add('active');
        }
        
        sessionList.appendChild(sessionElement);
    });
}

handlesessionclick

세션 버튼 클릭과 관련된 이벤트들을 따로 정리한 함수.

우선 각 세션 아이템 클릭과 관련하여, 클릭이벤트 발생 시 가장 가까운 session item을 탐색, child 요소를 클릭하더라도 세션 전체가 클릭되도록 하였다.

function handleSessionClick(event) {
    const sessionElement = event.target.closest('.session-item');
    if (!sessionElement) return;

다만 삭제 버튼의 경우 stopPropagation을 통해 클릭 시 세션이 선택되는 것을 막았다. 이후 해당 세션 ID를 불러온 뒤 삭제 모달을 표시하도록 하였다 (최종 삭제는 모달에서 삭제 버튼을 누른 후에 deletesession 함수에서 담당)

    const deleteButton = event.target.closest('[data-action="delete"]');
    if (deleteButton) {
        event.stopPropagation();
        const sessionId = sessionElement.getAttribute('data-session-id');
        showDeleteModal(sessionId);
        return;
    }

세션 클릭 시 sessionId를 가져온다. 이때 세션이 로딩 중일 경우 중복 클릭을 막는다.
세션이 로드되고 나면 로딩 class를 제거한다.

   const sessionId = sessionElement.getAttribute('data-session-id');
   if (sessionId && sessionId !== currentSessionId) {
       if (sessionElement.classList.contains('loading')) return;
       
       sessionElement.classList.add('loading');
       loadSession(sessionId).finally(() => {
           sessionElement.classList.remove('loading');
       });
   }
}

loadsession

특정 세션을 DynamoDB에서 불러와 로드한다.
만약 불러와야 하는 세션이 현재 세션과 동일하다면 함수를 종료한다.

async function loadSession(sessionId) {
    if (currentSessionId === sessionId) {
        return;
    }

먼저 이전 세션의 active 클래스를 제거하고 연결을 끊는다.

    const previousActive = document.querySelector('.session-item.active');
    if (previousActive) {
        previousActive.classList.remove('active');
    }

    if (currentSessionId) {
        await disconnectCurrentSession();
    }

그 후 현재 세션의 ID를 currentsessionId로 새롭게 지정하고, API 호출 전 토큰 유효성을 확인한다.

    currentSessionId = sessionId;

    try {
        const idToken = localStorage.getItem('auth_token');
        if (!idToken) {
            console.error('No auth token available');
            return;
        }

        await ensureValidToken();

REST API로 백엔드로부터 세션을 호출한다. 연결된 함수는 tarotchat_getsessionmessage.

       const response = await fetch(`${config.restEndpoint}/sessions/${sessionId}?userId=${userId}`, {
           headers: {
               'Authorization': `Bearer ${idToken}`
           }
       });
       const messages = await response.json();

응답 처리 부분. Websocket API를 연결한다. 연결에 성공하면 해당 session item에 active 클래스를 추가한다.

       if (!Array.isArray(messages)) {
           console.error('Unexpected response format');
           return; // 메시지가 비어 있으면 함수를 종료
       }
       await connectWebSocket();

       const newActive = document.querySelector(`.session-item[data-session-id="${sessionId}"]`);
       if (newActive) {
           newActive.classList.add('active');
       }

   } catch (error) {
       console.error('Error loading session:', error);
       // 에러 발생 시 롤백
       currentSessionId = null;
   }
}

StartNewChat

새 대화를 시작하기 위한 함수를 만들었다.
먼저 현재 연결된 ws가 있다면 이를 닫는다. disconnectcurrentsession 함수는 아래에서 다로 설명하겠다.

async function startNewChat() {
    if (socket) {
        socket.close();
    }
    
    if (currentSessionId) {
        await disconnectCurrentSession();
    }

현재 세션 ID를 null값으로 바꾸고, 이번에도 토큰 유효성을 확인한다.

    currentSessionId = null;
    
    try {
        const idToken = localStorage.getItem('auth_token');  // idToken 가져오기
        if (!idToken) {
            throw new Error('No auth token available');
        }

        await ensureValidToken();

rest API로 요청을 보내고 응답을 파싱한다. 연결된 함수는 tarotchat_newsession

        const response = await fetch(`${config.restEndpoint}/sessions`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${idToken}`
            },
            credentials: 'include',
            body: JSON.stringify({ userId: userId })
        });
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const result = await response.json();

현재 세션의 sessionID를 currentSessionId로 받고 세션 목록을 새로고침한다.

        if (result.sessionId) {
            currentSessionId = result.sessionId;
            await fetchSessions(userId); 

Websocket 연결을 수행한다.

            console.log('Attempting WebSocket connection...');
            await connectWebSocket();
            console.log('WebSocket connection established');
        } else {
            throw new Error('Session ID not received from server');
        }
    } catch (error) {
        console.error('Error in startNewChat:', error);
    }
}

disconnectcurrentsession

현재 세션과 소켓 연결을 끊는 단순한 코드이다.

async function disconnectCurrentSession() {
    if (socket) {
        socket.close();

connectwebsocket

반면 소켓 연결은 아래 함수가 담당한다.
먼저 localstorage에서 accessToken을 가져온다.

async function connectWebSocket() {
    const accessToken = localStorage.getItem('access_token');
    if (!accessToken || !userId || !currentSessionId) {
        console.error('Missing required parameters for WebSocket connection');
        throw new Error('WebSocket connection failed: Missing parameters');
    }

이어서 토큰 정보와 사용자 ID, 세션 ID 등 호출에 필요한 정보를 모두 URL에 담아 socket 개체를 정의한다.

    const wsUrl = `${config.wsEndpoint}?token=${accessToken}&userId=${userId}&sessionId=${currentSessionId}`;
    
    if (socket) {
        socket.close();
    }

    socket = new WebSocket(wsUrl);

리턴값에서는 웹소켓 상태에 따라 promise 개체를 리턴

    return new Promise((resolve, reject) => {
        socket.onopen = function() {
            resolve();
        };

        socket.onmessage = function(event) {
            const data = JSON.parse(event.data);
            handleIncomingMessage(data);
        };

        socket.onclose = function(event) {
            console.log('WebSocket closed:', event.code, event.reason);
        };

        socket.onerror = function(error) {
            console.error('WebSocket error:', error);
            reject(error);
        };

5초 타임아웃을 설정하여 에러 발생 시 과도한 대기를 일으키지 않고 에러 반환시킨다.

        const timeout = setTimeout(() => {
            socket.close();
            reject(new Error('WebSocket connection timeout'));
        }, 5000);

        socket.onopen = function() {
            clearTimeout(timeout);
            console.log('WebSocket connected for session:', currentSessionId);
            resolve();
        };
    });
}
profile
딴짓 좋아하는 데이터쟁이

0개의 댓글