SENTENCE U | Day 16 (useQuery 최적화/로컬폰트/웹소켓 최적화)

블로그 이사 완료·2023년 1월 26일
0
post-thumbnail

DIARY, REQUEST페이지 제외 하고 모든 UI 작성 완료.

홈페이지와 개인 프로필 페이지의 UI는 완성했다.

다이어리 페이지에는 캘린더와 유저 본인만 볼수있는 다이어리 리스트를 추가할 것이고, 요청페이지에는 관리자에게 보낼수있는 요청사항을 담은 Form을 추가할 것이다.

유저의 ID, 이름, 이미지, 접속상태 가져오는 useQuery 통합

기존에 서버에서 res.data를 가지고와 useQuery의 data에 할당해 객체의 값을 가져오려고 시도했었는데 계속 객체의 키에 접근이 불가능했다.

그래서 서버에서 받을때 특정 키값만 리턴받아 전부 따로 함수로 내보냈다.

import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

// 유저 로그인 상태
export const useUserLoginStatus = () => {
  const { data, isLoading, error } = useQuery(['userLoginStatus'], async () => {
    return await axios
      .get('/api/users')
      .then((res) => {
        if (res.data.isAuth === false) return false;
        if (res.data._id) return true;
      })
      .catch((error) => {
        console.log(error.response);
      });
  });
  return { data, isLoading, error };
};

// 유저 아이디
export const useUserId = () => {
  const { data } = useQuery(['userId'], async () => {
    return await axios
      .get('/api/users')
      .then((res) => {
        if (res.data._id) return res.data._id;
      })
      .catch((error) => {
        console.log(error.response);
      });
  });

  return { data };
};

export const useUserName = () => {
  const { data } = useQuery(['userName'], async () => {
    return await axios
      .get('/api/users')
      .then((res) => {
        if (res.data.userName) return res.data.userName;
      })
      .catch((error) => {
        console.log(error.response);
      });
  });

  return { data };
};

// 유저 이미지
export const useUserImage = () => {
  const { data } = useQuery(['userImage'], async () => {
    return await axios
      .get('/api/users')
      .then((res) => {
        if (res.data.userImage) return res.data.userImage;
      })
      .catch((error) => {
        console.log(error.response);
      });
  });

  return { data };
};

효율이 당연히 떨어질 수 밖에 없었다. 똑같은 API에 여러개의 함수로 호출을 보내니..

네트워크 자체에서도 allUser에만 요청을 수도 없이 보냈다.

Object.entries로 원래 객체를 배열로 변환할수 있는데 변환 할 수 없다는 에러가 계속 나왔다.

그래서 그냥 Object만 사용해서 객체에 직접 접근해서 키를 가져오니 해결되었다.

원인은 리턴받은 Query의 객체 데이터가 빈 객체일 경우에는 에러가 발생하는 것 같았다.

export const useGetClientUser = () => {
  const { data, isLoading, error, refetch } = useQuery(
    ['clientUser'],
    async () => {
      return await axios
        .get('/api/users')
        .then((res) => {
          return res.data;
        })
        .catch((error) => {
          console.log(error);
        });
    },
    {
      cacheTime: Infinity, // 캐싱 시간
      refetchInterval: false, // 리패치시간
    },
  );

  let isAuth;
  if (Object(data).isAuth === (null || undefined)) {
    isAuth = true;
  } else if (!Object(data).isAuth) {
    isAuth = false;
  }
  const userId = Object(data)._id;
  const userName = Object(data).userName;
  const userImage = Object(data).userImage;

  return { isAuth, userId, userName, userImage, isLoading, error, refetch };
};

웹폰트 타입에서 로컬폰트로 변경

폰트를 Google Font에서 웹폰트로 가져오는 방식을 사용했었다.

페이지가 로딩되면서 불러오는 시간 자체는 짧지만 폰트를 불러오기 전의 요청들로 인해 순서가 늦어서 잠시라도 폰트가 깨지는 현상을 볼수가 있었다.

그래서 웹폰트에서 실제로 사용하는 폰트만 로컬로 다운받아 직접 @font-face로 적용해줬다.

웹소켓 최적화

기존의 웹소켓에서 온라인 유저를 불러오는 코드는 유저리스트 컴포넌트 안에 있었다.

문제는 유저리스트 컴포넌트와, 상위컴포넌트들이 리렌더링 될 때마다 소켓과의 연결을 너무 많이 시도해 네트워크 요청을 많이 보낸다.

import { useAllUsers, useGetClientUser } from './userInfo';
import { useEffect, useState } from 'react';
import { io } from 'socket.io-client';

export const useSocket = () => {
  const { allUsers } = useAllUsers();
  const { userName } = useGetClientUser();
  const [socket, setSocket] = useState();
  const [onlineList, setOnlineList] = useState([]);
  const BACK_URL = 'http://localhost:8000';

  useEffect(() => {
    const options = {
      path: '/socket.io',
      cors: { origin: '*', credentials: true },
      transports: ['websocket'],
    };

    console.log('socket: connect');

    const socketIo = io.connect(`${BACK_URL}/online`, options);
    setSocket(socketIo); // 소켓 연결되면 따로 socket에 다시 저장

    if (userName) {
      socketIo?.on('userConnect', () => {
        socketIo?.emit('login', { userName: userName }); // 유저명 보냄
      });
    }

    return () => {
      console.log('socket: disconnet');
      socketIo?.disconnect(); // 이 코드 없으면 서버에 연결 많이 시도함
      socketIo?.off('userConnect');
    };
  }, [userName]);

  useEffect(() => {
    // 서버에서 온라인 리스트 배열로 받음
    socket?.on('onlineList', (data) => {
      // 배열에서 중복요소 제거해서 새로운 배열 생성
      const userArray = data.filter((ele, i) => {
        return data.indexOf(ele) === i;
      });
      // 새로운 배열 온라인리스트 state에 저장
      setOnlineList(userArray);
    });
    // 소켓 한 번 연결 됐으면 연결 끊기
    return () => {
      socket?.disconnect();
      socket?.off('onlineList');
    };
  }, [socket]);

  // 온라인 유저가 맨 위에 위치하도록 배열 순서 바꾸기
  let sortedUsers = [];
  allUsers.forEach((user, i) => {
    if (onlineList.indexOf(user) !== -1) {
      allUsers.splice(i, 1);
      sortedUsers.push(user);
    }
  });
  sortedUsers.push(...allUsers);

  return { onlineList, sortedUsers };
};

이 긴 코드를 useSocket.js로 모듈화 시켜고 이 함수 안에서 온라인 유저가 맨 위로 위치하도록 배열 순서까지 바꿔서 onLineList와 sortedUsers 데이터를 반환한다.

그러므로 유저리스트에서는 useSocket에서 요청한 데이터를 받아 렌더링만 시키면 됐다.

const UserLists = () => {
  const { onlineList, sortedUsers } = useSocket();
  // console.log('유저리스트 렌더링');
  return (
    <Container>
      <Title>유저 목록</Title>
      <ListWrap>
        {sortedUsers.map((user, i) => {
          const isOnline = onlineList.indexOf(user);
          return (
            <List className={isOnline < 0 ? 'offline' : 'online'} key={i}>
              <Link to={`/${user}`}>{user}</Link>
            </List>
          );
        })}
      </ListWrap>
      <Online>접속 중</Online>
    </Container>
  );
};

export default UserLists;

useQuery함수 최적화

서버에서 받아온 데이터를 전역적으로 사용하기 위해 react-query를 사용했는데, 취지와는 맞지 않게 상위 컴포넌트에서 쿼리 데이터를 받아 props로 내려주는 아주 바보같은 짓을 했다.

나중에 늦어서 전체적으로 props를 줄이는 작업을 하는 것보다 한시라도 빠를 때 원래 취지에 맞게 작업해야겠다고 생각했다.

userName,ID,Image 등 여러개의 쿼리키로 받아오는 쿼리함수들은 위에서 다뤘던 것처럼 하나로 묶었다.

const NavBar = () => {
  const { userName, userImage } = useGetClientUser();
  
  ...

NavBar로 예를 들자면 App컴포넌트에서 useQuery를 이용해 받아온 데이터를 props로 받아서 사용했는데 이는 전역관리와는 취지가 맞지 않았다.

그래서 NavBar 컴포넌트 자체에서 데이터를 받아 사용하는 방식으로 수정했다.

전체적으로 props를 줄이고 컴포넌트에서 직접 데이터를 받아옴으로써 코드들의 데이터 관리상태를 보기가 훨씬 쉬워졌다.


Gravatar삭제

유저가 처음 회원가입 할 때 default 프로필 사진을 정하기 위해서 Gravatar라이브러리를 사용했는데, [404, retro, robot] 등 많은 스타일로 지정할 수 있었다.

하지만 원하는 이미지들이 아니였고 결국 Gravatar에서 제공하는 기본 이미지를 선택했다.

하지만 기본적으로 url을 불러오려면 스키마에서도 지정해줘야 했고, 유저가 페이지 접속 시마다 gravatar로 요청을 해서 이 이미지를 불러와야 했다.

const userSchema = new mongoose.Schema(
  {
      userImage: {
      type: String,
      default: 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y',
    },

어차피 기본 이미지를 이렇게 심플한 걸로 선택할 거라면 인터넷에서도 쉽게 구할 수 있는 이미지를 로컬에서 사용하기로 했다.

그래서 서버에 유저가 설정한 이미지가 없다면 로컬에서 기본 이미지를 가져와 보여주도록했다.

<img
  alt={userName}
  src={userImage ? userImage : './src/assets/images/default.png'} />

댓글삭제기능

댓글의 삭제버튼 X는 포스트를 작성한 사람이거나, 댓글을 작성한 본인에게만 보인다.

{comment.commentUser === userName ||
  			postUser === userName ?
  						(<b id={comment.commentId} onClick={onDeleteComment}>X</b>)
					  : ('')
}

댓글을 삭제하면 포스트를 삭제할 때와 마찬가지로 confirm창으로 물어보고 true를 받았을 때 실행된다.

const onDeleteComment = useCallback((e) => {
  if (window.confirm('댓글을 삭제하시겠습니까?')) {
    axios
      .delete(`/api/posts/${containerRef.current.id}/comments/${e.target.id}`, {
      withCredentials: true,
    })
      .then((res) => {
      setCommentList(res.data.comments);
      setCommentCount(res.data.comments.length);
      toast.success('삭제 성공');
      setIsEditing(false);
    })
      .catch((error) => {
      console.log(error);
      toast.error('오류가 발생했습니다.');
    });
  }
}, []);

API로는 포스트와 삭제버튼에 각각 id를 부여했기 때문에 선택되어있는 포스트의 id와 선택한 댓글의 id를 보낸다.

<Container id={postId} ref={containerRef}>
<b id={comment.commentId} onClick={onDeleteComment}>

삭제하고 나면 서버에서 댓글이 삭제된 리스트를 바로 save해서 돌려주기 때문에 프론트에서 바로 반영할 수 있다.

이를 나는 state로 관리해서 바로 해당 포스트의 댓글만 리렌더링 되도록 했다.

.then((res) => {
  toast.success('작성 성공!');
  setCommentList(res.data.comments);
  setCommentCount(res.data.comments.length);
  setComment('');
  setIsEditing(false);
})

profile
https://kyledev.tistory.com/

0개의 댓글