🔗 마이페이지(1)

 

[팀프로젝트] 리액트로 뉴스피드 웹사이트 제작하기 - 마이페이지(1)

🔗 사전기획 [팀프로젝트] 리액트로 뉴스피드 웹사이트 제작하기 - 사전기획리액트로 첫 팀프로젝트가 시작됐다. 뭔가 프로젝트다운 프로젝트를 하는 것 같다. 인스타그램이나 페이스북,  블

ejunyang.tistory.com

 

 

 

 

 

✏️ 내가 쓴 게시글 불러오기

로그인한 아이디와 같은 유저 아이디를 가진 데이터의 모든 정보를 posts 테이블에서 가져온다.

 const { data: postData, error: postError } = await supabase
        .from('posts')
        .select('*')
        .eq('user_id', userData.email);
      setPosts(postData);

      if (postError) {
        console.error('게시글 정보를 가져오지 못했습니다.', postError);
        return;
      }

 

 

❤️ 좋아요 누른 게시글 불러오기

    const { data: likeData, error: likeError } = await supabase
        .from('user_info')
        .select('post_heart')
        .eq('user_id', userData.email);

      if (likeError) {
        console.error('좋아요한 게시글 정보를 가져오지 못했습니다.', likeError);
        return;
      }

 

JSON 형식의 문자열로 저장된 게시글 ID들이 들어있고, 이 문자열을 JSON.parse를 사용하여 JavaScript 배열로 변환한 다음 likedPostIds라는 변수에 저장한다.

 

let likedPostIds = JSON.parse(likeData[0].post_heart);

 

좋아요한 게시글 ID가 담겨있는 likedPostId를 map 을 돌려 새로운 배열을 반환한다. 각 게시물 ID (postId)에 대해 Supabase 데이터베이스에서 해당 ID와 일치하는 게시물의 세부 데이터를 가져온다. likedPostDetails 배열에서 null이 아닌 요소들만 필터링하여 setLikedPosts 함수로 전달한다.

      try {
        const likedPostDetails = await likedPostIds.map(async (postId) => {
            const { data: likedPostData, error } = await supabase
            .from('posts')
            .select('*')
            .eq('id', postId);

            if (error) {
              console.error('좋아요한 게시글의 상세 정보를 가져오지 못했습니다.', error);
              return null;
            }
            return likedPostData;
          })
        setLikedPosts(likedPostDetails.filter((post) => post !== null));
      } catch (error) {
        console.error('오류', error);
      }

2

 

👾 오류

likedPostIds가 배열이 아니라는 타입오류가 났다. 찾아보니 async 함수는 항상 Promise를 반환한다는 것을 알아냈다. 반환된 프로미스 객체를 비동기식으로 처리하기 위래 Promise all 메서드를 사용해보았다.

 

🔗 참고자료

 

Promise.all() 로 비동기 처리를 구현해 보자

- Promise.all() 은 여러 개의 Promise 들을 비동기적으로 실행하여 처리할 수 있다.

velog.io

 

 

 

💡 해결

흐름은 아래와 같다. 아래 흐름으로 간다면 Promise.all이 완료됐을 때, likedPostDetails는 각 postId에 대한 쿼리 결과의 배열이 된다.

  • Promise.all은 전달된 모든 Promise가 해결될 때까지 기다린 후, 그 결과를 배열로 반환
  • likedPostIds.map은 각 postId에 대해 비동기 작업을 수행하는 Promise 객체의 배열을 반환
  • 해당 배열을 Promise.all에 전달하여 모든 비동기 작업이 완료될 때까지 기다림
  • await를 사용하여 모든 Promise가 완료될 때까지 기다림

 

 const likedPostDetails = await Promise.all(
          likedPostIds.map(async (postId) => {
            const { data: likedPostData, error } = await supabase.from('posts').select('*').eq('id', postId);

            if (error) {
              console.error('좋아요한 게시글의 상세 정보를 가져오지 못했습니다.', error);
              return null;
            }
            return likedPostData;
          })
        );
        setLikedPosts(likedPostDetails.filter((post) => post !== null));
      } catch (error) {
        console.error('오류', error);
      }

 

 

 

📌 북마크한 게시글 불러오기

 const { data: saveData, error: saveError } = await supabase
        .from('user_info')
        .select('post_save')
        .eq('user_id', userData.email);
      setSavePosts(saveData);

      if (saveError) {
        console.error('북마크한 게시글 정보를 가져오지 못했습니다.', saveError);
        return;
      }

 

 

saveData[0]가 존재하는지 확인하고, 존재하지 않으면 undefined 처리. 옵셔널 체이닝으로(?) saveData[0]가 존재하지 않으면 undefined를 반환한다. 좋아요 게시글의 세부데이터를 가져오는 방식과 같은 방식이다.

 

let savePostIds = JSON.parse(saveData[0]?.post_save || '[]');
      try {
        const savePostDetails = await Promise.all(
          savePostIds.map(async (postId) => {
            const { data: savedPostData, error } = await supabase.from('posts').select('*').eq('id', postId);

            if (error) {
              console.error('북마크한 게시글의 상세 정보를 가져오지 못했습니다.', error);
              return null;
            }
            return savedPostData;
          })
        );
        setSavePosts(savePostDetails.filter((post) => post !== null));
      } catch (error) {
        console.error('오류', error);
      }
    };

createBrowserRouter

createBrowserRouter는 react router v6.4부터 사용할 수 있다.

 

 

 

라우터 생성

createBrowserRouter()

createBrowserRouter()에 라우팅 할 path element로 작성할 수 있다. children 속성으로 배열에 중첩된 라우터(Nested Router)를 추가해 줄 수 있다.

import { createBrowserRouter } from "react-router-dom";
import Login from "../pages/Login";
import Join from "../pages/Join";
import Layout from "../layout/Layout";
import Main from "../pages/Main";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      {
        path: "/",
        element: <Main />,
      },
      {
        path: "/login",
        element: <Login />,
      },
      {
        path: "/join",
        element: <Join />,
      },
    ],
  },
]);

 

 

 

 

App.jsx

최상단에서 <RouterProvider> 를 import한다.
RouterRrovider에는 router={} props를 필수로 넣어야하고, Router.jsx에서 createBrowserRouter 함수로 생성한 router를 넘겨준다.

import React from "react";
import { RouterProvider } from "react-router-dom";
import { router } from "./router/Router";

const App = () => {
  return <RouterProvider router={router} />;
};

export default App;

 

기존의 상태관리 라이브러리인 Redux는 제공하는 기능과 연계된 미들웨어 등 매우 강력한 퍼포먼스를 자랑하지만, 설정과 사용법이 복잡했다.  Zustand는 상태관리 본연의 기능에 집중하여 위와 같은 복잡성을 줄이고, 보다 간단하고 직관적인 상태관리 기능을 제공한다는 장점이 있다. 

📌 공식문서
단순화된 Flux 패턴을 사용하는 작고(small) 빠르고(fast) 확장가능한(scalable) 상태관리 솔루션이며, 
Hooks에 기반으로하는 간편한 API가 존재

 

 

 

 

설치

yarn add zustand

 

해당 경로로 파일 생성 후 세팅 초기값을 설정하고, 1씩 증가하는 로직과 초기화하는 로직을 만들었다.

// src > zustand > bearsStore.js
import { create } from "zustand";

const useBearsStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

export default useBearsStore;

 

 

 

사용

다른 컴포넌트에서 사용하는 방법은 useState, useEffect와 같은 기본 훅을 사용하는 것처럼 간편하게 사용할 수 있다.

// src > App.jsx
import "./App.css";
import useBearsStore from "./zustand/bearsStore";

function App() {
  const bears = useBearsStore((state) => state.bears);
  const increasePopulation = useBearsStore((state) => state.increasePopulation);
  return (
    <div>
      <h1>{bears} around here ...</h1>
      <button onClick={increasePopulation}>one up</button>
    </div>
  );
}
export default App;

심화프로젝트 사전 기획 중 스타일링은 어떤 라이브러리를 사용할껀지 논의하다가 강민 튜터님께서 next.js를 쓰면 Tailwind css는 자연스럽게 쓰는 추세라고 말씀해주셨다. 거진 고정적이라고.. 그래서 이번 프로젝트 때 테일윈드를 사용해보기로 합의했다. 한번도 써본적 없어서 좀 부담스럽지만 요새 계속 쓰는 추세라고하니 사용해보는게 좋을 것 같다.

 

 

 

TailWind CSS

성능 이슈와 클래스 네임 충돌 이슈를 줄이고, 유연하고 직관적인 스타일링을 제공한다. 공식 문서에서는 

유틸리티 퍼스트(Utility-First) CSS 프레임워크로, 빠르고 쉽게 스타일링을 적용할 수 있는 클래스를 제공한다 고 쓰여져있다.

📌 유틸리티 클래스란?
유틸리티 클래스는 특정 스타일 속성을 나타내는 짧고 간단한 `CSS 클래스`로, 
HTML 요소에 직접 적용하여 빠르고 쉽게 스타일링을 할 수 있습니다.
Tailwind CSS는 이러한 유틸리티 클래스를 대량으로 제공하여, 
별도의 CSS 작성 없이도 다양한 스타일을 구현할 수 있게 합니다. 
예를 들어,`bg-blue-500`은 배경색을 파란색으로, `p-4`는 패딩을 설정하는 유틸리티 클래스입니다.

 

 

 

성능

  • Tailwind CSS는 불필요한 스타일을 제거하고, 필요한 부분만 스타일을 적용하는 방식으로 성능 최적화
  • 사용하지 않는 CSS를 제거하여, 최종 빌드 파일 크기를 최소화
  • CSS 파일의 크기를 줄이고, 애플리케이션 로딩 속도를 개선
  • Tailwind CSS는 React의 JSX 문법과 함께 사용할 수 있어, 스타일링 간편
  • Tailwind 설정 파일(tailwind.config.js)을 통해 색상, 폰트, 스페이싱 등 다양한 설정을 커스터마이징
  • 다양한 설정 옵션을 제공하여, 프로젝트의 요구사항에 맞는 스타일링을 손쉽게 적용
📌 Purging CSS?
`Purging CSS`는 사용하지 않는 CSS를 제거하여 최종 빌드 파일 크기를 줄이는 과정을 의미합니다. 
Tailwind CSS는 이를 자동으로 처리해줍니다.

 

 

 

 

설치 및 기본 사용법

yarn add tailwindcss postcss autoprefixer

 

초기화 파일 생성

yarn tailwind init -p

tailwind.config.js

 

 

 

 

 

 

 

 

 

 

 

 

 

 

tailwind.config.js 수정

src 하위 파일 중 확장자가 .js, .jsx, .ts, .tsx인 파일을 대상으로 한다.

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

 

 

index.css

css에 적용하기

@tailwind base;
@tailwind components;
@tailwind utilities;

 

 

 

 

컴포넌트에 써먹기

inline-style

return (
    <div style={{display:"flex" flexDirection:"column" gap:"8px" marginBottom:"1.25rem"}} >
      {sortedPlace.map((place) => {
        return <PlaceItem key={place.id} place={place} />;
      })}
    </div>
  );

 

 

Tailwind CSS

return (
    <div className="flex flex-col gap-8 mb-20">
      {sortedPlace.map((place) => {
        return <PlaceItem key={place.id} place={place} />;
      })}
    </div>
  );

 

 

 

무조건 껐다 키기!!!!!!!!! 껐다 켜야지 적용된다 적용안된다고 울지마세요 엉엉 난 울었으니까..

 

 

 

🔗 사전기획

 

[팀프로젝트] 리액트로 뉴스피드 웹사이트 제작하기 - 사전기획

리액트로 첫 팀프로젝트가 시작됐다. 뭔가 프로젝트다운 프로젝트를 하는 것 같다. 인스타그램이나 페이스북,  블로그 같이 게시글을 쓰고 읽고 수정하고 삭제할 수 있는 뉴스피드 웹사이트를

ejunyang.tistory.com

 

 

 

 

 

 

마이페이지 구현

마이페이지에 필요한 기능을 정리해보았다.

1. 로그인한 사용자의 데이터 가져오기 => id, nickname, 프로필 이미지
2. 사용자가 쓴 게시글 데이터 가져오기
3. 사용자가 좋아요 누른 게시글 데이터 가져오기
4. 사용자가 저장한 북마크 게시글 데이터 가져오기

 

 

 

컴포넌트 분리

아래와 같이 컴포넌트를 나누었다.

 

 

 

 

사용자 데이터 가져오기

 

준혁님께서 Auth.api 를 만들어주셔서 getUser를 가지고와서 사용했다. 

export const getUser = async () => {
  try {
    const {
      data: { user },
      error
    } = await supabase.auth.getUser();

    if (error) {
      console.log('Error:', error);
      return null;
    }

    // console.log('User data:', user);
    return user;
  } catch (error) {
    console.log('Error:', error);
    return null;
  }
};

 

마이페이지에서 화면에 보여줘야할 정보가 프로필 이미지와 닉네임이었기때문에 프로필 이미지와 닉네임은 useState 를 사용해줬다. 그리고 이번 과제를 하면서 supabase를 처음 다루어보았는데 우리가 Auth 테이블과 member 테이블 두개를 만들어놓은 상태로 개발을 진행한 상태라서 프로필을 변경할 땐 두 테이블 다 데이터를 업데이트해줘야했다. 

 useEffect(() => {
    const fetchData = async () => {
      const userData = await getUser();
      setProfileUrl(userData.user_metadata.imageSrc);
      setUser(userData);
      setNickname(userData.user_metadata.nickname);

      if (!userData) {
        console.error('유저 정보를 가져올 수 없습니다.');
        return;
      }
      const { data: memberData, error: memberError } = await supabase
        .from('member')
        .select('*')
        .eq('user_id', userData.email);

      if (memberError) {
        console.error('회원정보를 가져오지 못했습니다.', memberError);
        return;
      }
    };
    fetchData();
  }, []);

 

 

 

 

프로필 수정하기

 

변경해야할 닉네임은 useRef로 처리해서 DOM에 접근할 수 있도록 했다. state로 처리 하기엔 인풋에 값을 변경할때마다 불필요한 렌더링이 일어나기 때문에 useRef로 처리하는게 좋겠다고 생각했다.

  useEffect(() => {
    const fetchData = async () => {
      const userData = await getUser();
      if (userData) {
        setUser(userData);
        setProfileImg(userData.user_metadata.imageSrc);
        nicknameRef.current ? (nicknameRef.current.value = userData.user_metadata.nickname) : '';
      } else {
        console.error('회원정보를 불러오지 못했습니다.', error);
      }
    };
    fetchData();
  }, []);

 

 

 

 

프로필 업데이트

const handleUpdateData = async (e) => {
    e.preventDefault();
    try {
      const newNickname = nicknameRef.current.value;
      const image = e.target.image.files[0];
      const userData = await getUser();
      const ImageData = await apiImg(image);

      if (!ImageData) {
        throw new Error('이미지 업로드 실패');
      }

      // auth 업데이트
      const { data: authData, error: AuthError } = await supabase.auth.updateUser({
        data: { nickname: newNickname, imageSrc: ImageData }
      });

      if (AuthError) {
        console.error('Auth 업데이트 실패', AuthError.message);
        return;
      }

      console.log('Auth 업데이트 성공', authData);
      setUser(authData);

      // member 테이블 업데이트
      const { data, error } = await supabase
        .from('member')
        .update({ user_name: newNickname, user_imageSrc: ImageData })
        .eq('user_id', userData.email)
        .select('*');

      if (error) {
        console.error('member 업데이트 실패', error.message);
        return;
      }

      console.log('member 업데이트 성공', data);
      navigate(-1);
    } catch (error) {
      console.error('업데이트 실패', error);
    }
  };

 

👾 오ㅗ류

지출내역리스트 코드를 작성하다 마주한 에러이다. 한참 고민하다가 if문 구문 순서를 바꾸어 보았다.

const ExesList = () => {
  const { selectedMonth } = useContext(ExpenseContext);
  const { userInfo } = useContext(AuthContext);

  const {
    data: expenses = [],
    isPending,
    error,
  } = useQuery({
    queryKey: ["expenses"],
    queryFn: getExpense,
  });

  const filteredExpenses = userInfo
    ? expenses.filter(
        (exe) => exe.month === selectedMonth && exe.user === userInfo.id
      )
    : [];

  const totalAmount =
    filteredExpenses && filteredExpenses.length > 0
      ? filteredExpenses.reduce((acc, cur) => {
          return (acc += cur.amount);
        }, 0)
      : 0;
      
      
  if (isPending) {
    return <div>로딩중입니다.</div>;
  }

  if (error) {
    return <div>데이터를 불러오지 못했습니다.</div>;
  }


  return (
    <div>
      <TotalTitle>
        {`${selectedMonth + 1}월`} 한 달 동안
        <strong>총 {totalAmount}원</strong>
        사용했어요.
      </TotalTitle>
      <ul>
        {filteredExpenses &&
          filteredExpenses.map((exe) => {
            return <ExesItem key={exe.id} exe={exe} />;
          })}
      </ul>
    </div>
  );
};

export default ExesList;

 

 

 

 

💡 해결

const ExesList = () => {
  const { selectedMonth } = useContext(ExpenseContext);
  const { userInfo } = useContext(AuthContext);

  const {
    data: expenses = [],
    isPending,
    error,
  } = useQuery({
    queryKey: ["expenses"],
    queryFn: getExpense,
  });

  const filteredExpenses = userInfo
    ? expenses.filter(
        (exe) => exe.month === selectedMonth && exe.user === userInfo.id
      )
    : [];

  if (isPending) {
    return <div>로딩중입니다.</div>;
  }

  if (error) {
    return <div>데이터를 불러오지 못했습니다.</div>;
  }

  const totalAmount =
    filteredExpenses && filteredExpenses.length > 0
      ? filteredExpenses.reduce((acc, cur) => {
          return (acc += cur.amount);
        }, 0)
      : 0;

 

이렇게 위치를 바꾸니까 해결됐다. 코드의 흐름을 보니 이 때 expenses 데이터가 아직 로딩되지 않았거나 에러가 발생했다면, filteredExpenses의 계산 과정에서 문제가 발생했던 것.  isPending 및 error 체크를 먼저 수행하도록 변경하면 데이터 로딩 상태와 에러 상태를 먼저 확인하고, 해당 상태에 맞는 UI를 렌더링할 수 있다.

 

따라서 isPending 및 error 체크 구문의 위치를 변경함으로써, 데이터 로딩 상태와 에러 상태를 먼저 확인하고 그에 따른 적절한 UI를 렌더링할 수 있게 되었다. 코드의 안정성과 사용성을 높이는 데 도움이 되는 것 같다!

이번 개인 프로젝트때 주로 사용했던 Tanstack Query에 대해 알아보자. 리액트는 정말 훅이 많다..고 항상 느낀다.. 이번 TIL은 리액트 쿼리에 대해 깊이는 아니지만 내가 이해한만큼 써보려고 한다.

 

 

Tanstack Query❓

💡 fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리

 

서버 상태를 관리하기 위한 라이브러리로, 데이터를 패칭하고 캐싱, 동기화, 무효화 등의 기능을 제공한다. 복잡하고 장황한 코드가 필요하지 않고 리액트 컴포넌트 내부에서 간단하고 직관적으로 API를 사용할 수 있으며 이전보다 비동기 로직을 간편하게 작성하고 유지보수성을 높일 수 있다.

 

 

 

 

 

사용하는 이유

비동기 로직의 복잡성 해결 필요성

기존의 방식은 Fetching 코드를 작성하고 데이터를 담아 둘 상태(useState) 생성, useEffect를 이용해 컴포넌트 마운트시 데이터를 Fetching 한 뒤 상태에 저장하였다. 리액트 쿼리를 사용하면 해당 로직의 복잡함을 해결할 수 있다.

 

서버 상태 관리의 어려움

서버 상태는 클라이언트 상태와 달리 캐싱, 동기화, 재검증 등 관리해야 할 요소가 많아 기존 방법으론 관리가 어려움.

 

 

 

 

주요기능

  • 데이터 캐싱: DB서버에 여러번 동일한 데이터를 요청하지 않고 캐싱하여 데이터를 빠르게 가져온다.
  • 자동 리페칭: 데이터가 변경되었을 때 자동으로 리페칭하여 최신 상태를 유지한다.
  • 쿼리 무효화: 특정 이벤트가 발생했을 때 쿼리를 무효화하고 데이터를 다시 가져온다.

 

데이터를 가져오고, 수정하고, 리프레시한다. 무조건 외우기!!

📌 get / Modify / refresh

get = useQuery
Modify = useMutation
refresh = invalidateQueries

 

 

 

 

라이브러리 설치

yarn add @tanstack/react-query

 

리액트 쿼리를 사용하려면 백엔드 데이터 베이스가 필요해서 json-server로 임시 데이터 베이스를 만들어준다.

yarn add json-server

 

 

 

단축명령어 설정

 

📂package.json에서 아래와 같이 설정해준다. 그러면 yarn json 입력시 서버가 켜진다.

{
  "scripts": {
	"json": "json-server --watch db.json --port 5000",
  }
}

 

단축 명령어 없이 서버를 키는 명령어는 아래와 같다.

json-server --watch db.json

 

 

 

 

db.json

📂src > 📄db.json

{
  "expenses": [
    {
      "id": "f70630fd-852d-4566-a830-9d318c22046a",
      "date": "2024-06-18",
      "item": "📚",
      "amount": 50000,
      "desc": "리액트",
      "month": 5,
      "user": "wnswns"
    },
  ]
}

 

 

 

 

QueryClient 생성

useQuery를 사용할 곳에 쿼리 클라이언트를 생성해준 뒤 Provider를 적용해준다. 이렇게하면 전역에서 사용할 수 있다.

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

 

 

 

 

데이터 가져오기 : useQuery()

useQuery에 쿼리 키비동기 함수를 인자로 받아 데이터를 가져오고, 로딩 상태, 오류 상태, 그리고 데이터를 반환한다.

여기서 쿼리키는 배열로 들어간다. 

useQuery({ queryKey: [쿼리키], queryFn: 비동기 함수 })

 

실무에서 자주 사용하는 쿼리키 이름

💡 꼭 알아야 하는 것
 - 다른 API는 다른 쿼리키를 사용한다. (ex. 목록 API와 상세 API는 다른 쿼리키를 사용한다)
 - 목록 페이지에선 `["posts"]` or `["movies"]` or `["todos"]`이렇게 쓰는 경우가 많음.
 - 상세 페이지에서는 `["posts", id]` 이렇게 쓰는 경우가 많음.
 - 페이지네이션의 쿼리키는 `["posts", page]` 이렇게 쓰는 경우가 많음

 

 

 

 

서버에서 데이터를 가져오는 API 함수를 만들어보자. 

import axios from "axios";

const JSON_SERVER_HOST = "http://localhost:4000";

export const getExpense = async () => {
  try {
    const response = await axios.get(`${JSON_SERVER_HOST}/expenses`);
    return response.data;
  } catch (error) {
    console.error(error);
    alert("데이터를 가져오지 못했습니다.");
  }
};

 

 

위에서 만든 API 함수를 useQuery에 적용해보자. 아래 코드로 적용하면 데이터 가져오는 로직 완성.

useQuery({ queryKey: ["expense"], queryFn: getExpense })

 

위 코드를 아래 코드처럼 쓸 수 있다.

  • data : 화면에 보여줄 데이터 (queryFn의 return 값)
  • isLoading : 데이터 로딩 여부 (true, false)
  • error : 데이터 로딩 중 발생한 에러 데이터
const { data, isLoading, error } = useQuery({
  queryKey: ["expense"],
  queryFn: getExpense,
});

 

데이터를 받아오기 전 로딩중으로 뜨고, 데이터를 가져오지 못할 경우 예외 처리를 해줄 수 있다.

  if (isLoading) {
    return <div>로딩중입니다.</div>;
  }

  if (error) {
    return <div>데이터를 불러오지 못했습니다.</div>;
  }

 

 

 

 

데이터 추가, 수정, 삭제하기 : useMutation()

export const postExpense = async (newExes) => {
  try {
    const response = await axios.post(`${JSON_SERVER_HOST}/expenses`, newExes);
    return response.data;
  } catch (error) {
    console.error(error);
    alert("데이터를 생성하지 못했습니다.");
  }
};

 

 

다른 컴포넌트에서 리액트 쿼리를 써야할 경우 ✔️ useQueryClient() 훅을 사용해줘야한다. invalidateQueries 속성을 사용해주기 위해선 필수이다. 생성 후 데이터를 다시 가져와야하기 때문에 invalidateQueries를 사용해줬다. 기존에 있던(데이터를 생성하기전) 데이터는 무효화시키고 최신 상태(데이터가 추가된)를 가져온다

  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: postExpense,
    onSuccess: () => {
      queryClient.invalidateQueries(["expenses"]);
    },
  });

 

 

새로운 데이터가 추가되는 함수에 mutation.mutate 사용해주면 추가하는 로직도 완성이다. 차례대로 수정과 삭제도 같은 방법으로 하면 된다!

const onInsert = useCallback(
    (date, item, amount, desc) => {
      const newExes = {
        id: uuidv4(),
        date,
        item,
        amount,
        desc,
        month: selectedMonth,
        user: userInfo.id,
      };

      mutation.mutate(newExes);
    },
    [mutation, userInfo]
  );

 

리액트로 첫 팀프로젝트가 시작됐다. 뭔가 프로젝트다운 프로젝트를 하는 것 같다. 인스타그램이나 페이스북,  블로그 같이 게시글을 쓰고 읽고 수정하고 삭제할 수 있는 뉴스피드 웹사이트를 구현하는 프로젝트이다. 그리고 처음으로 서버를 사용해서 작업한다!! supabase를 사용하는데 프론트엔드 애플리케이션에 백엔드 서비스를 제공하기 위해 사용하는 모델이다. 

 

 

 

 

 

 

와이어프레임

전직 웹 퍼블리셔 + 디자인을 했었던 터라 디자인은 모두 내가 맡아서 진행했다. 어떤 뉴스피드를 할까 의견이 많이 나왔는데 1. 크림같은 중고거래 웹사이트, 2. 코디한 옷 공유하는 사이트, 3. 아이스크림 후기 공유하는 사이트 세가지가 나왔다. 우리 팀명이 구구콘이라 3번으로 탕탕탕ㅋㅋㅋㅋ 아이스크림 후기를 공유? 뭔가 생소할 수 있지만 정해진 주제이니 완전 열심히 해보았다. 디자인은 아이스크림 공유하는 사이트 답게 뭔가 파스텔 톤의 귀여운 디자인으로 뽑아봤다.

 

차례대로 헤더 -> 배너 -> 게시물 -> 푸터 순이고 우리 팀의 핵심 기능은 카테고리 별 필터링 하는 기능이다. 카테고리는 브랜드별, 맛별, 종류별로 나뉘어진다. 인기 게시물과 최근 게시물 모두 카테고리 별로 확인이 가능하다.

 

 

 

 

 

사전 기획

우리는 맛있는 아이스크림을 추천하는 게시물을 작성하고 다른 사람들의 후기를 통해 아이스크림 정보를 탐색하고 나의 아이스크림 취향을 모으는 마이페이지를 주제로 정하고 구현하기로 했다. 프로젝트 시작 전 와이어프레임 제작과 함께 사전 기획을 했다. 어떤 페이지에 어떤 컴포넌트와 기능이 필요한지. 데이터 형식은 어떤식으로 넣는게 좋을지 의논하여 정하였다. 

 

 

 

 

 

 

 

역할 분담

 

내가 맡은 역할은 마이페이지. 로그인 회원 정보를 가지고 와서 데이터를 뿌려주고 회원정보를 수정한다. 수정하는 데이터는 닉네임과 프로필 이미지이다. 더해서 전체적인 웹페이지의 반응형을 구현하기로 했다.

 

 

 

 

 

 

기술적 의사결정

아래와 같이 어떤 라이브러리를 사용할껀지 팀원들과 사전에 협의하고 진행했다.

 

 

 

 

 

 

 

개발 프로세스

+ Recent posts