โœ๏ธ What I Learned/TIL

[WIL] ์‹ฌํ™” ํ”„๋กœ์ ํŠธ - ์ฑ—๋ด‡, private route, ๋งˆ์ดํŽ˜์ด์ง€ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋™์ž‘์›๋ฆฌ ๋ฐ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ์ •๋ฆฌ

Jiwon() 2023. 8. 11. 22:15

๐Ÿ’ก WIL 20230807-20230811 : TypeScript React - ์ฑ—๋ด‡, private route, ๋งˆ์ด ํŽ˜์ด์ง€ ๊ตฌํ˜„ํ•œ ๊ธฐ๋Šฅ ๋™์ž‘์›๋ฆฌ ๋ฐ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ์ •๋ฆฌ


1. ๋งก์€ ๊ธฐ๋Šฅ

  • openai API ์‚ฌ์šฉํ•˜์—ฌ ์ฑ—๋ด‡ UI ๊ธฐ๋Šฅ ๊ตฌํ˜„
  • auth ์—ฌ๋ถ€์— ๋”ฐ๋ผ ์ ‘๊ทผ ๋ถˆ๊ฐ€๋Šฅํ•˜๋„๋ก private route ์„ค์ •
  • ๋งˆ์ดํŽ˜์ด์ง€ ํ”„๋กœํ•„ ์ˆ˜์ • ๋ฐ ๋‚ด๊ฐ€ ์“ด ๊ธ€ ๋ชจ์•„๋ณด๊ธฐ ํŽ˜์ด์ง€๋„ค์ด์…˜

 

 


2. ์‚ฌ์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

react-route-dom, redux-toolkit, react-query, supabase, openai, react-hook-form, styled-components

 

 


3. ๊ตฌํ˜„ํ•œ ๊ธฐ๋Šฅ ๋™์ž‘ ์›๋ฆฌ

1) ๐Ÿค–๐Ÿ’ฌ ์ฑ—๋ด‡ ๊ธฐ๋Šฅ - openai API, UI ์ƒํƒœ๊ด€๋ฆฌ

(1) openai API

  • openai API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ž…๋ ฅ prompt๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ๋‹ต๋ณ€์„ ์–ป์„ ์ˆ˜ ์žˆ์Œ
// lib - openai.ts
import { Configuration, OpenAIApi } from 'openai';

const configuration = new Configuration({
  apiKey: process.env.REACT_APP_OPENAI_API_KEY
});

export const openai = new OpenAIApi(configuration);
// components - ChatBot.tsx

const handlePromptSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setLoading(true);
    dispatch(
      addChatLog({
        id: shortid.generate(),
        role: 'user',
        chat: prompt
      })
    );
    setPrompt('');
    try {
      const { data } = await openai.createCompletion({
        model: 'text-davinci-003',
        prompt,
        temperature: 0.8,
        max_tokens: 256
      });
      if (data?.choices[0]?.text) {
        dispatch(
          addChatLog({
            id: data.id,
            role: 'bot',
            chat: data.choices[0]?.text
          })
        );
      } else {
        alert('์ •์ƒ์ ์œผ๋กœ ์ „๋‹ฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
      }
    } catch (err) {
      alert('์ •์ƒ์ ์œผ๋กœ ์ „๋‹ฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
    }
    setLoading(false);
  };

 

(2) ์ฑ„ํŒ… UI

  • ์‚ฌ์šฉ์ž์˜ ํ”„๋กฌํ”„ํŠธ์™€ openai ๋‹ต๋ณ€์„ redux toolkit store์— ๋ฐฐ์—ด๋กœ ์ €์žฅ
  • ๋ฐฐ์—ด์˜ ์š”์†Œ(์ฑ„ํŒ… ๋กœ๊ทธ)๋“ค์„ useSelector๋กœ ๊ฐ€์ ธ์˜จ ๋’ค, map์œผ๋กœ role === 'bot'๊ณผ role === 'user'๊ฐ€ ์ฐจ๋ก€๋กœ ๋ Œ๋”๋ง๋˜๊ฒŒ ํ•˜์—ฌ ์ฑ„ํŒ…์ฒ˜๋Ÿผ ๋ณด์ด๋„๋ก UI ๊ธฐ๋Šฅ ๊ตฌํ˜„
// components - ChatBot.tsx

import React, { useRef, useState, useEffect } from 'react';
import { openai } from '../../lib/openai';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { RootState } from '../../redux/config/configStore';
import { addChatLog } from '../../redux/module/chatBotLogSlice';
import shortid from 'shortid';
import LoaderIcon from 'remixicon-react/Loader2LineIcon';
import SendPlaneIcon from 'remixicon-react/SendPlaneFillIcon';
import * as S from '../../styles/StChatBot';

const ChatBot = () => {
  const { user } = useAppSelector((state: RootState) => state.user);
  const [prompt, setPrompt] = useState('');
  const [loading, setLoading] = useState(false);
  const chatBotLogs = useAppSelector((state) => state.chatBotLog.logs);
  const dispatch = useAppDispatch();
  const submitButtonRef = useRef(null);

  const chatAreaRef = useRef<HTMLDivElement>(null);
  // ์ฑ„ํŒ… ๋กœ๊ทธ๊ฐ€ ์Œ“์ด๋ฉด ์Šคํฌ๋กค ์ž๋™์œผ๋กœ ์•„๋ž˜๋กœ ๊ฐ€๊ฒŒ๋”
  useEffect(() => {
    if (chatAreaRef.current) {
      chatAreaRef.current.scrollTop = chatAreaRef.current.scrollHeight;
    }
  }, [chatBotLogs]);

  const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setPrompt(e.currentTarget.value);
  };

  // ์งˆ๋ฌธ์„ ์ „์†กํ•˜๋Š” textarea์—์„œ ์—”ํ„ฐ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ์ค„๋ฐ”๊ฟˆ์ด ์•„๋‹Œ submit์ด ๋˜๋„๋ก
  const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key === 'Enter') {
      e.preventDefault();
      if (submitButtonRef.current) {
        (submitButtonRef.current as HTMLButtonElement).click();
        setPrompt('');
      }
    }
  };

  const handlePromptSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setLoading(true);
    dispatch(
      addChatLog({
        id: shortid.generate(),
        role: 'user',
        chat: prompt
      })
    );
    setPrompt('');
    try {
      const { data } = await openai.createCompletion({
        model: 'text-davinci-003',
        prompt,
        temperature: 0.8,
        max_tokens: 256
      });
      if (data?.choices[0]?.text) {
        dispatch(
          addChatLog({
            id: data.id,
            role: 'bot',
            chat: data.choices[0]?.text
          })
        );
      } else {
        alert('์ •์ƒ์ ์œผ๋กœ ์ „๋‹ฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
      }
    } catch (err) {
      alert('์ •์ƒ์ ์œผ๋กœ ์ „๋‹ฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
    }
    setLoading(false);
  };

  return (
    <>
      <S.ChatContainer>
        <S.ChatTitle>๐Ÿค” ๋ณ€์ˆ˜๋ช…, ํ•จ์ˆ˜๋ช…์„ ๋ฌผ์–ด๋ณด์„ธ์š”!</S.ChatTitle>
        <S.ChatArea ref={chatAreaRef}>
          <S.ChatLogBox>
            <S.RoleName>์ฝ”๋ฆฐ๋ด‡ ๐Ÿ˜</S.RoleName>
            <S.BotChatLog>
              ์•ˆ๋…•ํ•˜์„ธ์š”! <br /> ๋ณ€์ˆ˜๋ช…, ํ•จ์ˆ˜๋ช…์„ ๊ณ ๋ฏผ ์ค‘์ด์‹ ๊ฐ€์š”? ์ €์—๊ฒŒ ๋ฌผ์–ด๋ณด์„ธ์š”!
            </S.BotChatLog>
          </S.ChatLogBox>
          {chatBotLogs.map((chat) => {
            if (chat.role === 'bot') {
              return (
                <S.ChatLogBox key={chat.id}>
                  <S.RoleName>์ฝ”๋ฆฐ๋ด‡ ๐Ÿ˜</S.RoleName>
                  <S.BotChatLog>{chat.chat}</S.BotChatLog>
                </S.ChatLogBox>
              );
            } else if (chat.role === 'user') {
              return (
                <S.UserPromptBox key={chat.id}>
                  <S.RoleName>{user ? user.name : ''} ๋‹˜</S.RoleName>
                  <S.UserChatLog>{chat.chat}</S.UserChatLog>
                </S.UserPromptBox>
              );
            }
            return null;
          })}
        </S.ChatArea>
        <S.PromptArea>
          <S.PromptForm onSubmit={handlePromptSubmit}>
            <S.PromptInput
              typeof="text"
              value={prompt}
              onChange={onChange}
              onKeyDown={onKeyDown}
              maxLength={64}
              placeholder="์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์„ธ์š”!"
              required
            />
            <S.PromptSubmitButton type="submit" ref={submitButtonRef} disabled={!prompt || loading}>
              {loading ? <LoaderIcon /> : <SendPlaneIcon />}
            </S.PromptSubmitButton>
          </S.PromptForm>
        </S.PromptArea>
      </S.ChatContainer>
    </>
  );
};

export default ChatBot;

 

2) ๐Ÿ”โ†ช๏ธ private route ๊ธฐ๋Šฅ - react-router-dom

  • store์—์„œ user ์ƒํƒœ๊ฐ€ user object์ธ์ง€ null์ธ์ง€ ๊ฐ€์ ธ์™€์„œ user ๊ฐ์ฒด๊ฐ€ ์กด์žฌํ•  ๋•Œ(=๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์žˆ์„ ๋•Œ)๋งŒ ๊ธ€ ์ž‘์„ฑ ํŽ˜์ด์ง€์™€ ๋งˆ์ด ํŽ˜์ด์ง€์ธ '/write', '/mypage' url์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ธฐ๋Šฅ ๊ตฌํ˜„
  • user ๊ฐ์ฒด๊ฐ€ ์—†๋Š” ์ƒํƒœ(user : null)์—์„œ 'korini-project.vercel.app/write' ๋˜๋Š” 'korini-project.vercel.app/mypage'๋กœ ์ ‘๊ทผํ•  ์‹œ main ํŽ˜์ด์ง€(korini-project.vercel.app/)๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋˜๋„๋ก ํ•˜์˜€์Œ
// Router.tsx
const Router = () => {
  const { user } = useAppSelector((state: RootState) => state.user);
 
  // ์ค‘๊ฐ„ ์ƒ๋žต
 
{/* PrivateRoute : ๋กœ๊ทธ์ธ ์œ ์ € ์—†์œผ๋ฉด main ํŽ˜์ด์ง€๋กœ ์ด๋™ */}
          <Route element={<PrivateRoute />}>
            <Route path="/mypage" element={<Mypage />} />
            <Route path="/write" element={<Write />} />
          </Route>
        </Route>
        <Route path="*" element={<NotFound />} />
// PrivateRoute.tsx

import { Outlet, Navigate } from 'react-router-dom';
import { useAppSelector } from '../hooks';
import { RootState } from '../redux/config/configStore';

const PrivateRoute = () => {
  const { user } = useAppSelector((state: RootState) => state.user);
  return user ? <Outlet /> : <Navigate to="/" />
};

export default PrivateRoute;

 

 


3) ๐Ÿ—‚๏ธ๐Ÿ“ƒ ๋งˆ์ด ํŽ˜์ด์ง€ - supabase API, react-hook-form, pagination

(1) ํ˜„์žฌ ์œ ์ €์˜ ์ƒํƒœ ๊ด€๋ฆฌ

  • ์œ ์ € auth ์ƒํƒœ๋ฅผ ์ตœ์ƒ๋‹จ ์ปดํฌ๋„ŒํŠธ์ธ App.tsx์—์„œ supabase api๋กœ ๋ถˆ๋Ÿฌ์˜จ ๋’ค, ๋กœ๊ทธ์ธ ํ•œ ์œ ์ €๊ฐ€ ์žˆ์œผ๋ฉด ์œ ์ €์˜ uid, email, ๋‹‰๋„ค์ž„์„ store์— user state๋กœ ๊ด€๋ฆฌํ•˜์—ฌ ๊ฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ „์—ญ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ด€๋ฆฌ

 

(2) ๋‹‰๋„ค์ž„ ์ˆ˜์ •

  • ์ด ๋•Œ, supabase Auth์—๋Š” firebase Auth์˜ displayName๊ณผ ๋‹ฌ๋ฆฌ ๋‹‰๋„ค์ž„ ์š”์†Œ๊ฐ€ ์—†์–ด์„œ, supabase DB์— ๋”ฐ๋กœ user ์ •๋ณด๋ฅผ ๋“ฑ๋กํ•˜์—ฌ Auth๊ฐ€ ์•„๋‹Œ user DB์—์„œ ๋‹‰๋„ค์ž„ ๊ด€๋ฆฌ
  • ๋‹‰๋„ค์ž„ ์ˆ˜์ • ์š”์ฒญ ์‹œ ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ uid์™€ supabase user db์— ์ €์žฅ๋˜์–ด์žˆ๋Š” uid๋ฅผ ๋น„๊ตํ•˜์—ฌ ๋‹‰๋„ค์ž„ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†ก/์ˆ˜์ •
  • ์ˆ˜์ • ํ›„์—๋Š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ฐ”๋กœ ๋ Œ๋”๋ง๋  ์ˆ˜ ์žˆ๋„๋ก store์˜ user.name์—๋„ dispatch๋กœ ์ „๋‹ฌํ•˜์—ฌ ํ™”๋ฉด์— ๋ณ€๊ฒฝ ํ›„ ๋‹‰๋„ค์ž„์ด ๋ฐ”๋กœ ๋ฐ˜์˜๋  ์ˆ˜ ์žˆ๋„๋ก ํ•จ

 

(3) ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ

  • react-hook-form ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ(๋น„๋ฐ€๋ฒˆํ˜ธ ์ตœ์†Œ ๊ธธ์ด ๋ฐ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ์ž…๋ ฅ), ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ•ธ๋“ค๋ง ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์˜€๊ณ , ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผํ•˜๋ฉด supabase Auth์— ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๋ณ€๊ฒฝ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ „์†กํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Œ
// ChangePassword.tsx

import supabase from '../../lib/client';
import { useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';

import * as S from '../../styles/StMyPage';
import * as G from '../../styles/StButton';

interface IFormInput {
  password: string;
  passwordCheck: string;
}

const ChangePassword = () => {
  const { register, handleSubmit, formState } = useForm<IFormInput>();

  const onSubmit = async (formValues: IFormInput) => {
    try {
      const { data, error } = await supabase.auth.updateUser({
        password: formValues.password
      });

      if (error) {
        if (error.message === 'New password should be different from the old password.') {
          // ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ด์ „ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๊ฐ™์€ ๊ฒฝ์šฐ
          window.alert('๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋ณ€๊ฒฝ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
          return false;
        } else {
          window.alert('๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
          return false;
        }
      }
      if (data) {
        window.alert('๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
        return false;
      }
    } catch (error) {
      window.alert('๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
      return false;
    }
  };

  return (
    <>
      <S.MyPageForm onSubmit={handleSubmit(onSubmit)}>
        <S.LabelInputBox>
            <label htmlFor="password">๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ</label>
          <S.MyPageInput
            id="password"
            type="password"
            {...register('password', {
              required: '์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.',
              minLength: {
                value: 6,
                message: '๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 6์ž ์ด์ƒ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'
              }
            })}
          />
        </S.LabelInputBox>
        <ErrorMessage
          errors={formState.errors}
          name="password"
          render={({ message }) => <S.MyPageErrorMsg>{message}</S.MyPageErrorMsg>}
        />
        <S.LabelInputBox>
          <label htmlFor="passwordCheck">๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ํ™•์ธ</label>
          <S.MyPageInput
            id="passwordCheck"
            type="password"
            {...register('passwordCheck', {
              validate: (value, formValues) =>
                value === formValues.password || '๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'
            })}
          />
        </S.LabelInputBox>
        <ErrorMessage
          errors={formState.errors}
          name="passwordCheck"
          render={({ message }) => <S.MyPageErrorMsg>{message}</S.MyPageErrorMsg>}
        />
        <G.Button type="submit">๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ</G.Button>
      </S.MyPageForm>
    </>
  );
};

export default ChangePassword;

 

(4) ๋‚ด๊ฐ€ ์“ด ๊ธ€ ๊ธฐ๋Šฅ

  • uid๋ฅผ ํ™œ์šฉํ•˜์—ฌ supabase ๊ฒŒ์‹œ๊ธ€ db์— ๋‹ด๊ฒจ์žˆ๋Š” ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž uid๋ฅผ ๋น„๊ตํ•ด์„œ, ํ˜„์žฌ ์œ ์ €์˜ ๊ฒŒ์‹œ๊ธ€๋งŒ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๊ฐ€์ ธ์™€์„œ ๋งˆ์ดํŽ˜์ด์ง€์—์„œ ๋ชจ์•„์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„

 

(5) ํŽ˜์ด์ง€๋„ค์ด์…˜

  • ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ ๊ธ€ ๋ฐ์ดํ„ฐ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๊ฐœ์ˆ˜๋ฅผ ๋”ฐ๋กœ api๋กœ ๊ฐ€์ ธ์˜ด
  • useState๋กœ currentPage๋ฅผ ๊ด€๋ฆฌ
  • ์œ ์ €๊ฐ€ ์ž‘์„ฑํ•œ ์ „์ฒด ๊ธ€ ๊ฐœ์ˆ˜, ํ•œ ํŽ˜์ด์ง€๋‹น ๊ฒŒ์‹œ๊ธ€(5๊ฐœ ์ƒ์ˆ˜๋กœ ์„ค์ •), currentPage์™€ ํ•จ๊ป˜ react-js-pagination ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŽ˜์ด์ง€๋„ค์ด์…˜ UI ๊ตฌํ˜„
// api - post.ts

// ๋‚ด๊ฐ€ ์“ด ๊ธ€ ์กฐํšŒ
export const ITEMS_PER_PAGE = 5;
const getMyPosts = async (userid: string, pageNum: number): Promise<PostType[]> => {
  const startIndex = (pageNum - 1) * ITEMS_PER_PAGE;
  const { data } = await supabase
    .from('post')
    .select('*')
    .eq('userid', userid)
    .range(startIndex, startIndex + ITEMS_PER_PAGE - 1)
    .order('date', { ascending: false });
  return data as PostType[];
};

// ๋‚ด๊ฐ€ ์“ด ๊ธ€ ์ „์ฒด ๊ฐœ์ˆ˜
const getMyPostsNum = async (userid: string) => {
  if (!userid) return null;
  const { count } = await supabase.from('post').select('*', { count: 'exact' }).eq('userid', userid);
  return count ?? 0;
};
// components - mypage - MyPosts.tsx
import { useNavigate } from 'react-router-dom';
import { useAppSelector } from '../../hooks';
import { RootState } from '../../redux/config/configStore';
import { useQueryClient, useQuery } from '@tanstack/react-query';
import { ITEMS_PER_PAGE, getMyPosts, getMyPostsNum } from '../../api/post';
import { useState, useEffect } from 'react';
import Pagination from 'react-js-pagination';
import Loading from '../layout/Loading';
import { PostType } from '../../types/types';

import * as S from '../../styles/StMyPage';
import * as P from '../../styles/StPageButton';

const MyPosts = () => {
  const navigate = useNavigate();
  const { user } = useAppSelector((state: RootState) => state.user);

  const [currentPage, setCurrentPage] = useState(1);
  const { data: myPostsNum } = useQuery(['myPostsNum'], () => getMyPostsNum(user?.userid ?? ''));

  const maxPostPage = Math.ceil(myPostsNum ? myPostsNum / ITEMS_PER_PAGE : 1);

  // ํ”„๋ฆฌํŽ˜์นญ
  const queryClient = useQueryClient();
  useEffect(() => {
    if (currentPage < maxPostPage) {
      const nextPage = currentPage + 1;
      queryClient.prefetchQuery(['myPosts', nextPage], () => getMyPosts(user?.userid ?? '', nextPage));
    }
  }, [currentPage, queryClient]);

  // ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
  const { isLoading, data: myPosts } = useQuery<PostType[]>(['myPosts', currentPage], () =>
    getMyPosts(user?.userid ?? '', currentPage)
  );

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

  return (
    <>
      <S.MainPostsContainer>
        {myPosts?.map((myPost) => {
          return (
            <S.box key={myPost.postid}>
              <S.PostBox
                onClick={() => {
                  // ์ ˆ๋Œ€๊ฒฝ๋กœ
                  navigate(`/detail/${myPost.postid}`);
                }}
              >
                <S.PostBoxNav>
                  <div>{myPost.title}</div>
                  <S.Nickname>{myPost.name}</S.Nickname>
                </S.PostBoxNav>
                <S.PostContentBox>{myPost.body}</S.PostContentBox>
              </S.PostBox>
            </S.box>
          );
        })}
      </S.MainPostsContainer>
      <P.PageLists>
        <Pagination
          activePage={currentPage}
          itemsCountPerPage={ITEMS_PER_PAGE}
          totalItemsCount={myPostsNum ?? 0}
          pageRangeDisplayed={5}
          prevPageText={`โ—€`}
          nextPageText={`โ–ถ`}
          onChange={setCurrentPage}
        />
      </P.PageLists>
    </>
  );
};

export default MyPosts;

 

 


4. ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

private route ๊ด€๋ จ

(1) ๋กœ๊ทธ์ธ ๋œ ์œ ์ €๊ฐ€ ์žˆ์„ ๋•Œ์—๋„ ์ฃผ์†Œ์ฐฝ์— ์ง์ ‘ '/write'๋‚˜ '/mypage'๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ์ ‘๊ทผํ•˜๋ ค๊ณ  ํ•˜๋ฉด, main('/')์œผ๋กœ ํŠ•๊ฒจ์ ธ ๋‚˜์˜ค๋Š” ํ˜„์ƒ

  • ์›์ธ
    • private route ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•˜๋Š” user ์ƒํƒœ๋ฅผ rtk store์—์„œ ๋ฐ›์•„์˜ค๊ธฐ ๋•Œ๋ฌธ์—, url์— ์ง์ ‘ ์ž…๋ ฅํ•˜๋ฉด ํŽ˜์ด์ง€๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจ๋˜์–ด store์—์„œ user๋ฅผ ๋ฐ”๋กœ ๋ฐ›์•„์˜ค์ง€ ๋ชปํ•˜๊ณ  ๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์—†๋Š” ๊ฒƒ์œผ๋กœ ์ธ์‹ํ•˜์—ฌ ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ํŠ•๊ฒจ์ ธ๋‚˜์˜ด
  • ํ•ด๊ฒฐ๋ฐฉ์•ˆ ๋ชจ์ƒ‰
    • localstorage์—์„œ auth-token๊ฐ’์„ getItemํ•ด์„œ auth-token๊ฐ’์˜ ์œ ๋ฌด๋กœ auth ์œ ์ €์˜ ์กด์žฌ๋ฅผ ๋ฐ›์•„์˜ค๊ณ ์ž ํ•จ
  • ์‹คํŒจ
    • supabase์˜ ๊ฒฝ์šฐ auth-token์˜ key๊ฐ’์ด ์ฃผ๊ธฐ์ ์œผ๋กœ ๋ฐ”๋€Œ๊ธฐ ๋•Œ๋ฌธ์—, ์ด ๋ฐ”๋€Œ๋Š” ๊ฐ’์„ ์–ด๋–ป๊ฒŒ ๊ฐ€์ ธ์™€์•ผํ• ์ง€ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ์„ ์ฐพ์ง€ ๋ชปํ•ด์„œ ์ตœ์ข…์ ์œผ๋กœ๋Š” ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์ง€ ๋ชปํ•˜์˜€์Œ