뭉균의 개발일지

[프로젝트 회고] 일정 관리 웹 애플리케이션 Taskify - 2주차 본문

프로젝트 회고

[프로젝트 회고] 일정 관리 웹 애플리케이션 Taskify - 2주차

박뭉균 2024. 11. 3. 22:11

🚪 들어가며

 

이번 포스팅은 Taskify 개발 2주차를 진행하며, 해당 주에 개발하고 공부한 내용을 정리하기위해 작성했습니다.

혹시 1주차 회고를 보고 싶은 분들은 해당 링크를 참고해주세요! (링크: https://mungyun.tistory.com/28)

 

[프로젝트 회고] 일정 관리 웹 애플리케이션 Taskify - 1주차

🚪 들어가며  저는 코드잇 스프린트 프론트 엔드 9기 수료 중이며, 2024년 10월 18일(금)부터 2024년 11월 5일(화)까지 약 3주간 중급 프로젝트를 진행합니다. 이 기간 동안 4명의 팀원들과 함께 일정

mungyun.tistory.com

 

 

⚒️ 나의 역할

 

1. 할 일 수정 Modal 디자인 및 기능 구현 

 

할 일 수정 Modal

 

 

할 일 수정 Modal의 디자인을 위의 그림과 같이 구현했습니다. 이 Modal은 UpdateTodoForm컴포넌트로 상태를 관리했습니다. 모든 input을 입력하고 수정버튼을 클릭하면 UpdateCard함수를 통해 서버에 put요청을 보내고 요청이 성공하면 서버에도 수정된 카드의 정보가 저장됩니다.

 

 

2. Zustand를 통한 Card 상태 관리

 

저희 팀은 상태 관리 라이브러리를 많이 사용해보지않아 이번 프로젝트를 통해 사용해보길 원했습니다. 많은 선택지가 있었지만 그 중 Zustand가 문법과 사용법이 간단하여 이것을 통해 상태 관리하기로 결정했습니다. 저는 할 일 생성 또는 할 일 수정 Modal을 통해 반영되는 Card 데이터를 관리하는 cardsStore를 만들어봤습니다. 이 Store를 통해 Card를 추가 또는 수정, 삭제를 할 때 바로 화면에 반영되도록 구현할 수 있었습니다. 

 

interface CardsStore {
  cards: { [columnId: number]: CardType[] };
  fetchCards: (columnId: number) => Promise<void>;
  addCard: (columnId: number, newCard: CardType) => void;
  updateCard: (columnId: number, updatedCard: CardType) => void;
  deleteCard: (columnId: number, cardId: number) => Promise<void>;
}

const useCardsStore = create<CardsStore>((set) => ({
  cards: {},

  fetchCards: async (columnId) => {
    try {
      const cardData = await getCards({ columnId });
      set((state) => ({
        cards: { ...state.cards, [columnId]: cardData.cards },
      }));
    } catch (err) {
      console.error("카드를 불러오는데 실패했습니다.", err);
    }
  },

  addCard: (columnId, newCard) =>
    set((state) => ({
      cards: {
        ...state.cards,
        [columnId]: [...(state.cards[columnId] || []), newCard],
      },
    })),

  updateCard: (columnId, updatedCard) =>
    set((state) => ({
      cards: {
        ...state.cards,
        [columnId]: state.cards[columnId]?.map((card) =>
          card.id === updatedCard.id ? updatedCard : card
        ),
      },
    })),

  deleteCard: async (columnId, cardId) => {
    try {
      await deleteCard(cardId);
      set((state) => ({
        cards: {
          ...state.cards,
          [columnId]: state.cards[columnId]?.filter(
            (card) => card.id !== cardId
          ),
        },
      }));
    } catch (err) {
      console.error("카드를 삭제하는 데 실패했습니다.", err);
    }
  },
}));

export default useCardsStore;

 

 

3. 에러 시, 서버 측 에러 메시지를 보여주는 useErrorModal 훅 구현 

 

예를 들어, 대시보드의 맴버가 아닌데 대시보드의 수정을 요청한 경우 다음과 같은 message가 서버로 부터 response됩니다.

{
  "message": "대시보드의 멤버가 아닙니다."
}

 

이렇게 서버에서 받은 error message를 클라이언트에서 보여주기 위해 useErrorModal 훅을 만들었습니다. error를 매개변수로 받아 error message를 return하여 Modal로 띄워주므로써, 사용자에게 어떤 error가 발생했는지 알려줄 수 있었습니다. 

useErrorModal 사용 예시

 

 

4. My페이지 기능 구현 

 

My페이지

 

프로필을 관리하는 MyProfile 컴포넌트와 비밀번호 변경을 관리하는 MyPassword 컴포넌트로 해당 페이지를 구성했습니다.

MyProfile에서는 저장된 프로필 및 유저정보는 SSR로 미리 불러오게 설정했고 MyPassword에서는 현재 비밀번호가 정확하고 새 비밀번호와 새 비밀번호 확인이 일치하는 경우, 비밀번호가 수정되게 설정했습니다. 이 조건이 충족되지 않으면 서버에서 error message를 response해주고 이를 위에서 만든 useErrorModal을 통해 유저에게 알려줬습니다. 

이 과정을 진행하며 프로필 정보를 SSR로 불러오려고 시도하는 중 새로운 내용을 알게 되었습니다.

 

 

MyProfile 컴포넌트 SSR로 변경

 

서버에서 유저 정보를 가져오는 과정에서 쿠키가 제대로 적용되지 않으면, 인증이 필요한 API 호출이 실패할 수 있습니다. Next.js에서 getServerSideProps는 서버에서 실행되기 때문에, 클라이언트 측에서 설정된 쿠키는 기본적으로 사용할 수 없습니다.

서버에서 쿠키를 사용하여 인증된 요청을 하려면, 클라이언트의 쿠키를 서버에 전달해야 합니다. 이를 위해 몇 가지 방법이 있습니다.

 

 

쿠키를 서버에서 직접 사용하기

 

getServerSideProps에서 context.req를 통해 클라이언트의 쿠키에 접근할 수 있습니다. 저는 nookies 패키지를 사용하여 쿠키를 읽었습니다. 코드는 아래와 같이 구현했습니다. 

 

import { GetServerSideProps } from "next";
import { parseCookies } from "nookies"; // nookies를 사용하여 쿠키 파싱
import { getUserInfo } from "@/utils/api/authApi";

export const getServerSideProps: GetServerSideProps = async (context) => {
  const cookies = parseCookies(context); // 쿠키 파싱

  try {
    // 쿠키에서 필요한 인증 토큰을 가져오기
    const token = cookies.token; // 쿠키 이름에 맞게 수정

    const profileData = await getUserInfo(token); // 서버에서 프로필 데이터 가져오기
    return {
      props: { profileData }, // 데이터를 props로 전달
    };
  } catch (error) {
    throw error
    return {
      props: { profileData: null }, // 오류 발생 시 null 전달
    };
  }
};

 

이 과정을 통해 SSR로 프로필 데이터를 렌더링하는데 성공했습니다.

 

 

5. SEO 설정 

 

프로젝트를 진행하며 UI를 잘만드는 것 이상의 퍼포먼스를 보여주고 싶다는 생각이 들어 알아보던 중 성능 최적화에 대해 접하고 공부했습니다.

 

Lighthouse를 이용한 성능 최적화 시도

 

Lighthouse라는 확장자를 사용하여 현재 페이지의 성능을 분석하고 개선시킬 방법을 찾아 코드에 직접 적용하며 성능 최적화를 위해 힘썼습니다.

 

또한, SEO를 통한 성능 최적화를 시도했습니다. 아래 코드와 같이 MetaHead 컴포넌트를 만들어 각 페이지에서 그 페이지를 적절히 설명할 수 있는 페이지명과 소개를 적으며 SEO를 설정했습니다. og태그의 image, title, description을 props로 받아 사용합니다.

import Head from "next/head";

interface MetaHeadProps {
  title: string;
  description: string;
  imageUrl?: string;
}

const MetaHead: React.FC<MetaHeadProps> = ({
  title,
  description,
  imageUrl = "/thumbnail.png",
}) => {
  return (
    <Head>
      <title>{title}</title>
      <meta property="og:image" content={imageUrl} />
      <meta property="og:title" content={title} />
      <meta property="og:description" content={description} />
    </Head>
  );
};

export default MetaHead;

 

테스트 배포를 하고 확인하니 아래와 같이 Meta태그가 잘 적용된 모습을 볼 수 있었습니다. 

해당 링크는 테스트 배포 과정에서 사용되어 현재는 유효하지 않습니다.

 

 

6. UX개선 작업

 

테스트 배포 후, 직접 웹 애플리케이션을 사용해보며 불편한 점 또는 개선했으면 하는 부분을 정리하여 이후 리펙토링하는 과정을 거쳤습니다. 

 

 

✅ 이미지 드래그 문제

 

위의 사진이 드래그되는 문제

 

위의 그림에서 볼 수 있듯이 렌더링된 이미지가 드래그되는 문제가 있었습니다. 개인적으로 놓친 부분이지만 멘토님께서 이 부분을 지적해주셨고 추후 수정했습니다. 간단한 작업이지만 놓치기 쉬운 부분인것 같아 기록을 남겨둡니다. 아래 코드와 같이 draggable 속성을 false로 설정해주면 해당 문제가 해결되었습니다.

 

<Image
  src="/images/icons/icon_add_column.svg"
  width={16}
  height={16}
  alt="할 일 추가"
  draggable="false" // 이미지 드래그 방지
/>

 

 

CardList 스크롤바 추가

 

CardList에 보라색 커스텀 스크롤바를 추가했습니다.

 

만약 On progress 컬럼에만 많은 카드가 쌓이면 다른 컬럼은 비었음에도 스크롤바를 많이 내려야했고 그런 상황에서 다른 컬럼은 빈화면이여서 디자인적으로 불만스러웠습니다. 디자인을 임의로 수정해도된다고 허락을 받았기에 길이에 긴 컬럼에 한해서 커스텀 스크롤바를 추가하여 디자인 및 UX 개선을 시도했습니다.

 

 

react-spinner를 활용한 로딩창 구현

 

CSR로 데이터를 불러올 때, 데이터가 로딩되는 동안 보여줄 loading-spinner를 react-spinner라이브러리를 활용하여 LoadingSpinner라는 컴포넌트로 만들어 관리했습니다. 

 

데이터 로딩 중 렌더링되는 LoadingSpinner

 

 

 

서버 데이터 반영 방식에 대한 견해

 

CardList에 카드를 추가하거나 수정할 때, 클라이언트 측 상태 업데이트 후 서버 반영하거나 서버에 요청 후 새 데이터로 전체 목록을 다시 렌더링하는 방식에 대해 고민되었습니다. 후자의 경우, 새로고침처럼 깜빡거리는 과정이 있어 UX를 저하시킨다고 생각하여 저는 전자로 구현했습니다. 위에서 Card를 관리하기 위해 CardsStore를 만든 것도 이를 위해서 입니다. 

 

멘토링 시간에 해당 고민을 멘토님께 여쭤봤고 멘토님께서는 optimistic update라는 키워드를 알려주셨습니다. 이와 함께 추후 react-query를 학습하여 해당 부분의 경험을 쌓을 것입니다. 

 

 

🎀 마무리

2주차 활동을 진행하며 좋았던 점과 배운 점을 정리하며 이번 포스팅을 마무리하겠습니다. 

 

 

 

📌 새로운 라이브러리 사용으로 상태 관리 능력 향상


Zustand를 통해 간결하게 Card 데이터를 관리하며 클라이언트 측에서 변화된 데이터를 실시간으로 반영하는 방법을 경험했습니다. 다양한 상태 관리 옵션 중 적합한 라이브러리를 고르는 과정과 프로젝트 요구사항에 맞춰 효율적으로 활용하는 방법을 익힌 점이 좋았습니다.

 

 

📌 SEO와 SSR 적용으로 페이지 최적화 경험


SEO를 위한 MetaHead 컴포넌트를 구성하고 페이지를 설명할 수 있는 메타 태그를 설정하며, 웹 애플리케이션 최적화를 위한 기본적인 경험을 쌓았습니다. SSR로 프로필 데이터를 미리 불러오는 과정에서 클라이언트와 서버의 쿠키 차이를 이해하고, 쿠키를 서버에서 적절히 다루는 방법을 배울 수 있었습니다.

 

 

📌 UX 개선 작업과 사용자 경험의 중요성 인식


UI의 작은 요소도 사용자 경험에 큰 영향을 미칠 수 있음을 알게 되었고, 더욱 사용자 친화적인 인터페이스를 만드는 방법을 고민하게 되었습니다.

 

 

📌 Optimistic Update의 필요성 인식과 향후 학습 계획


서버에 데이터를 반영하는 방식에 대해 고민하면서, 멘토님이 언급한 'optimistic update'의 필요성을 느꼈습니다. react-query 학습 계획을 세우며, 상태 변화가 즉각적으로 반영되는 사용자 경험을 제공할 방법을 모색하게 되었습니다.