Personal Project Retrospective - Wereads

Seong Ho Kim·2024년 1월 3일
0
post-thumbnail

💻 Wereads 개인 프로젝트 소개

  • wereads 프로젝트는 사람들이 자신의 생각과 리뷰를 공유하고 소통하기 위한 커뮤니티 플랫폼 입니다.

✅ Wereads 개인 프로젝트 목적

  • 그동안 팀 프로젝트에 참여하면서 배웠던 스택들을 스스로 잘 적용했는지 점검하기 위한 목적으로 진행하였습니다.

👍 Wereads Github 저장소

여기를 클릭하시면 Wereads Project가 저장된 원격 저장소로 이동할 수 있습니다.

🕖 프로젝트 기간 & 참여인원

  • 날짜 : 2023.11.27 ~ 2023.12.27
  • 참여인원 : 1명 (Front-End)

🛠️ 기술 스택

  • Front-End : React, HTML, SCSS, Javascript, Github, Git, Visual Studio Code

📃 Wereads 기능/요구 정의서

1) 로그인

  • 요구사항 : 이메일(ID)과 비밀번호(PW)를 이용하여 서비스 가입자 여부를 확인하고 서비스를 이용할 수 있는 유저 토큰을 발급해주세요.
  • 필수 데이터 : email, password

2) 회원가입

  • 요구사항 : wereads 서비스를 이용하기 위해 회원가입 절차를 진행해 주세요.
  • 필수 데이터 : email, password, passwordConfirm

3) 메인 쓰레드 목록(List)

  • 요구사항 : 다른 사람들이 남긴 쓰레드들을 최신순으로 확인할 수 있게 해주세요.
  • 필수 데이터 : nickname, content, createdAt, updateAt

4) 쓰레드(포스트) 글 작성하기

  • 요구사항 : 로그인한 사용자가 쓰레드를 남길 수 있도록 해주세요.
  • 필수 데이터 : nickname, content, createdAt

5) 쓰레드(포스트) 수정 하기

  • 요구사항 : 본인이 남긴 포스트를 수정 할 수 있게 해주세요.
  • 필수 데이터 : content

6) 쓰레드(포스트) 삭제 하기

  • 요구사항 : 본인이 남긴 포스트를 삭제 할 수 있게 해주세요.

7) 댓글 기능

  • 요구사항 : 로그인 한 사용자가 포스트 상세 페이지에서 댓글을 추가할 수 있도록 해주세요

🏃🏻 맡은 역할(기능구현)

  • 혼자서 UI와 기능들을 만들어 보기에 기능/요구 정의서와 Figma를 참조해서 만들었습니다.(참고로 API 통신은 백엔드 없이 진행했던 관계로 Mock Data를 사용했습니다.)

(1) Front-End 초기 셋팅

  • 터미널로 CRA 및 프로젝트 폴더 생성
  • 로그인, 회원가입, 쓰레드 목록, 포스트 작성, 포스트 수정 Mock Data 생성
  • react-router-dom, sass 라이브러리 설치
  • 각 페이지별 Router 셋팅
  • Prettier 설치

(2) 로그인 페이지

  • 이메일 & 비밀번호 입력 전, 로그인 버튼 비활성화 상태 (로그인 불가능)

  • 이메일 & 비밀번호 입력 후, 로그인 버튼 활성화 상태 (로그인 가능)

React 코드 - 로그인 페이지)

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import UserInput from '../../Components/UserInput';
import UserButton from '../../Components/UserButton';
import './Login.scss';

const Login = () => {
  // 유저 정보(ID, PW)
  const [userInfo, setUserInfo] = useState({
    email: '',
    password: '',
  });
  
  // 이메일, 비밀번호
  const handleInputChange = event => {
    const { name, value } = event.target;
    setUserInfo(userInfo => ({
      ...userInfo,
      [name]: value,
    }));
  };
  
  // 유효성 검사
  const isVaild =
    userInfo.email.includes('@') &&
    userInfo.email.includes('.') &&
    userInfo.password.length >= 6;
    
  // 페이지 이동
  const navigate = useNavigate();
  // 회원가입 페이지 이동
  const goSignupPage = () => {
    navigate('/signup');
  };
  
  // fetch : 로그인 버튼 클릭시 메인 페이지로 이동
  const handleLogin = () => {
    fetch('/data/Login.json', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userInfo.email,
        password: userInfo.password,
      }),
    })
      .then(response => response.json())
      .then(data => {
        if (data.message === 'LOGIN SUCCESS') {
          alert('로그인 되었습니다.');
          localStorage.setItem('token', data.token);
          navigate('/main');
        } else {
          alert('가입되지 않은 정보입니다.');
        }
      });
  };

  return (
    <div className="login">
      <div className="userFrame" onChange={handleInputChange}>
        <div className="imageFrame">
          <img className="logo" src="/images/Logo.svg" alt="위코드 로고" />
          <img
            className="logo"
            src="/images/logo_wecode.svg"
            alt="위코드 로고"
          />
        </div>
        <UserInput
          type="text"
          placeholder="이메일"
          value={userInfo.email}
          name="email"
          onChange={handleInputChange}
        />
        <UserInput
          type="password"
          placeholder="비밀번호"
          value={userInfo.password}
          name="password"
          onChange={handleInputChange}
        />
        <UserButton text="로그인" disabled={!isVaild} onClick={handleLogin} />
        <div className="buttonWrapper">
          <button className="actionButton" onClick={goSignupPage}>
            회원 가입
          </button>
          <div className="wall">|</div>
          <button className="actionButton">비밀번호 찾기</button>
        </div>
      </div>
    </div>
  );
};

export default Login;
  • 로그인 페이지는 이메일, 비밀번호 입력창과 로그인 버튼, 회원가입 & 비밀번호 찾기 버튼으로 구성
  • 이메일은 이메일 형식으로, 비밀번호는 6자리 이상일때 유효성 검사를 부여하여 로그인 버튼 활성화
  • 회원가입 하기 & 비밀번호 찾기 버튼은 색깔별로 구분해서 표현했고, 회원가입 하기 버튼을 클릭했을때 회원가입 페이지로 이동
  • 로그인 시도시에 Fetch 메서드를 통해 유저 정보를 백엔드 API로 요청해서 확인후에 응답을 받아오는 방식으로 기능 구현 (실제 백엔드 API가 아닌 Mock Data를 이용했습니다.)

로그인 - Mock Data)

// Login.json
{
  "email": "wecode@gmail.com",
  "password": "123456"
}

Fetch 메서드 - Mock Data 연결)

   fetch('/data/Login.json', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userInfo.email,
        password: userInfo.password,
      }),
    })
      .then(response => response.json())
      .then(data => {
        if (data.message === 'LOGIN SUCCESS') {
          alert('로그인 되었습니다.');
          localStorage.setItem('token', data.token);
          navigate('/main');
        } else {
          alert('가입되지 않은 정보입니다.');
        }
   });
  • 참고로 실제 API와 통신할때는 데이터의 message가 'LOGIN SUCCESS'이면 로그인에 성공했다는 비동기 응답을 받아오는 방식으로 되어 있지만, 현재는 Mock Data로 연결되어 있기 때문에 실제 API 응답을 받아올 수 없으며 fetch를 이렇게 사용해서 통신하는 방식으로만 참고해주시면 되겠습니다.

(3) 회원가입 페이지

  • 필수사항 정보(이메일, 비밀번호, 비밀번호 확인) 입력 전, 회원가입 버튼 비활성화 상태

  • 필수사항 정보(이메일, 비밀번호, 비밀번호 확인) 입력 후, 회원가입 버튼 활성화 상태
    (선택 사항은 유저가 필수로 작성하지 않아도 되며, 선택해서 작성할 수 있습니다.)

React 코드 - 회원가입 페이지)

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import UserInput from '../../Components/UserInput';
import UserButton from '../../Components/UserButton';
import './Signup.scss';

const Signup = () => {
  // 이미지 업로드
  const [imageName, setImageName] = useState('');
  // 유저 정보(필수 사항)
  const [userInfo, setUserInfo] = useState({
    email: '',
    password: '',
    passwordConfirm: '',
    nickname: '',
  });
  
  // 이미지 업로드 input
  const handleFileChange = event => {
    event.stopPropagation();
    const file = event.target.files[0];
    if (file) {
      setImageName(file.name);
    }
  };

  // 이메일, 비밀번호, 비밀번호 확인
  const handleInputChange = event => {
    const { name, value } = event.target;
    setUserInfo(userInfo => ({
      ...userInfo,
      [name]: value,
    }));
  };
  
  // isVaild 변수 업데이트
  const isVaild =
    userInfo.email &&
    userInfo.password.length >= 10 &&
    userInfo.password === userInfo.passwordConfirm;
    
  // 로그인 페이지 이동
  const moveNavigate = useNavigate();
  const goToLogin = () => {
    moveNavigate('/');
  };
  
  // 회원가입 로직
  const processSignUp = () => {
    fetch('/data/Signup.json', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userInfo.email,
        password: userInfo.password,
        nickname: userInfo.nickname,
      }),
    })
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error('서버 응답이 올바르지 않습니다.');
      })
      .then(data => {
        if (data.message === 'SIGNUP SUCCESS') {
          moveNavigate('/signup-complete');
        } else {
          alert('회원가입에 실패했습니다. 다시 시도해주세요.');
        }
      })
      .catch(error => {
        alert('회원가입 중 오류가 발생했습니다. 다시 시도해주세요.');
      });
  };

  return (
    <div className="signup">
      <div className="registrationFrame" onChange={handleInputChange}>
        <div className="backButtonFrame">
          <img
            className="backIcon"
            src="/images/Back_arrow.svg"
            alt="뒤로버튼"
          />
          <button className="backButton" onClick={goToLogin}>
            뒤로
          </button>
        </div>
        <h1 className="titleText">회원가입</h1>
        <div className="infoTextFrame">
          <p className="userinfoText">기본 정보</p>
          <p className="infoOptionalText">필수 사항</p>
        </div>
        <div className="userInputFrame">
          <UserInput
            type="text"
            placeholder="이메일"
            value={userInfo.email}
            name="email"
            onChange={handleInputChange}
          />
          <UserInput
            type="password"
            placeholder="비밀번호"
            value={userInfo.password}
            name="password"
            onChange={handleInputChange}
          />
          <UserInput
            type="password"
            placeholder="비밀번호 확인"
            value={userInfo.passwordConfirm}
            name="passwordConfirm"
            onChange={handleInputChange}
          />
        </div>
        <div className="etcUserFrame">
          <div className="infoTextFrame">
            <p className="userinfoText">닉네임</p>
            <p className="infoOptionalText">선택 사항</p>
          </div>
          <input
            className="nicknameInput"
            type="text"
            placeholder="닉네임"
            value={userInfo.nickname}
            name="nickname"
            onChange={handleInputChange}
          />
          <div className="fileInputFrame">
            <label className="fileButton">
              <span className="selectFileText">파일 선택</span>
              <input
                className="fileInput"
                type="file"
                accept="image/*"
                onChange={handleFileChange}
              />
            </label>
            <input
              className="fileDisplay"
              placeholder="파일을 선택해 주세요"
              value={imageName}
              readOnly
            />
          </div>
          <div className="numberFrame">
            <div className="infoTextFrame">
              <p className="userinfoText">전화번호</p>
              <p className="infoOptionalText">선택 사항</p>
            </div>
            <div className="numberSelectFrame">
              <select className="numberBox">
                {PHONENUMBER_LIST.map((number, index) => (
                  <option key={index}>{number}</option>
                ))}
              </select>
              <input
                className="numberInput"
                type="text"
                placeholder="휴대폰 번호를 입력해주세요"
              />
            </div>
          </div>
          <div className="birthdayFrame">
            <div className="infoTextFrame">
              <p className="userinfoText">생일</p>
              <p className="infoOptionalText">선택 사항</p>
            </div>
            <div className="birthdaySelectFrame">
              <select className="birthdayBox yearBox">
                {BIRTHDAY_YEAR_LIST.map((year, index) => (
                  <option key={index}>{year}</option>
                ))}
              </select>
              <select className="birthdayBox monthBox">
                {BIRTHDAY_MONTH_LIST.map((month, index) => (
                  <option key={index}>{month}</option>
                ))}
              </select>
              <select className="birthdayBox dayBox">
                {BIRTHDAY_DAY_LIST.map((day, index) => (
                  <option key={index}>{day}</option>
                ))}
              </select>
            </div>
          </div>
          <div className="signupButtonFrame">
            <UserButton
              disabled={!isVaild}
              onClick={processSignUp}
              text="회원 가입"
            />
          </div>
        </div>
      </div>
    </div>
  );
};

export default Signup;

// 생일 데이터 : 년,월,일
const BIRTHDAY_YEAR_LIST = Array.from(
  { length: 15 },
  (_, i) => `${i + 1990}년`,
);
const BIRTHDAY_MONTH_LIST = Array.from({ length: 12 }, (_, i) => `${i + 1}월`);
const BIRTHDAY_DAY_LIST = Array.from({ length: 31 }, (_, i) => `${i + 1}일`);
// 휴대폰 데이터 : (010)
const PHONENUMBER_LIST = ['010', '011', '016', '018', '019'];
  • 회원가입 페이지는 미가입 유저가 필수 정보 입력란과 선택 정보 입력란으로 구성하여 미가입 유저가 쉽게 회원가입 할 수 있도록 유도
  • 이메일, 비밀번호, 비밀번호 확인 input에 정보들을 입력하면 회원가입 버튼 활성화
  • 선택사항은 닉네임과 프로필에 들어갈 이미지 파일선택, 생년월일, 전화번호를 선택해서 사용할수 있도록 유도
  • select Box를 사용한 생년월일, 전화번호와 같은 반복 UI들은 상수 데이터와 map 메서드를 활용하여 처리
  • 뒤로 버튼을 누르면 로그인 페이지로 이동

회원가입 - 상수 데이터)

// 생일 데이터 : 년,월,일
const BIRTHDAY_YEAR_LIST = Array.from(
  { length: 15 },
  (_, i) => `${i + 1990}년`,
);
const BIRTHDAY_MONTH_LIST = Array.from({ length: 12 }, (_, i) => `${i + 1}월`);
const BIRTHDAY_DAY_LIST = Array.from({ length: 31 }, (_, i) => `${i + 1}일`);
// 휴대폰 데이터 : (010)
const PHONENUMBER_LIST = ['010', '011', '016', '018', '019'];

회원가입 - Mock Data)

{
  "email": "wecode@gmail.com",
  "password": "123456",
  "nickname": "Apple"
}

Fetch 메서드 - Mock Data 연결)

// 회원가입 로직
  const processSignUp = () => {
    fetch('/data/Signup.json', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userInfo.email,
        password: userInfo.password,
        nickname: userInfo.nickname,
      }),
    })
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error('서버 응답이 올바르지 않습니다.');
      })
      .then(data => {
        if (data.message === 'SIGNUP SUCCESS') {
          moveNavigate('/signup-complete');
        } else {
          alert('회원가입에 실패했습니다. 다시 시도해주세요.');
        }
      })
      .catch(error => {
        alert('회원가입 중 오류가 발생했습니다. 다시 시도해주세요.');
      });
  };
  • 회원가입 통신 로직을 담당하는 fetch 에서 회원가입 정보를 모두 입력하고 회원가입 버튼을 누르게 되면 /data/Signup.json'에 들어있는 Mock Data의 정보를 body로 넘겨주고 body는 json 파일을 백엔드 API로 요청해 data의 메세지가 'SIGNUP SUCCESS' 라는 응답을 받아오면 회원가입 완료 페이지로 이동하고 만약에 실패하면 회원가입에 실패했다는 알림을 사용자에게 표시하도록 되어 있습니다. (현재는 Mock Data로 연결되어 있어 실제 API를 연결했을때 결과 값을 별도로 추후에 확인예정입니다.)

(4) 회원가입 완료 페이지

React 코드 - 회원가입 완료 페이지)

import React from 'react';
import { useNavigate } from 'react-router-dom';
import UserButton from '../../Components/UserButton';
import './SignupComplete.scss';

const SignupComplete = () => {
  const moveNavigate = useNavigate();
  // 확인 버튼 (로그인 페이지 이동)
  const goToLogin = () => {
    moveNavigate('/');
  };
  // 뒤로 버튼 (회원가입 페이지 이동)
  const goToSignup = () => {
    moveNavigate('/signup');
  };
  return (
    <div className="signupcomplete">
      <div className="backButtonFrame">
        <img className="backIcon" src="/images/Back_arrow.svg" alt="뒤로버튼" />
        <button className="backButton" onClick={goToSignup}>
          뒤로
        </button>
      </div>
      <div className="completeFrame">
        <img
          className="completelogo"
          src="/images/banner_square.svg"
          alt="완료체크"
        />
        <h1 className="mainText">회원 가입되었습니다!</h1>
        <p className="subText">이제 로그인해주세요.</p>
        <div className="buttonFrame">
          <UserButton text="확인" onClick={goToLogin} />
        </div>
      </div>
    </div>
  );
};

export default SignupComplete;
  • 회원가입 완료 페이지는 다음과 같이 회원가입 페이지에서 회원가입 하기 버튼을 누르면 회원 가입이 완료되었다는 표시를 사용자에게 보여주고 확인 버튼을 누르면 로그인 페이지로 이동
  • 뒤로 버튼을 누르면 회원가입 페이지로 이동

(5) 메인 쓰레드 List 페이지

React 코드 - 메인 쓰레드 List 페이지)

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import UserButton from '../../Components/UserButton';
import './MainThreadList.scss';

const MainThreadList = () => {
  // 포스트 리스트 관리
  const [postList, setPostList] = useState([]);
  // 유저 토큰 가져오기
  const userToken = localStorage.getItem('token');
  // 페이지 이동
  const navigate = useNavigate();

  // 포스트 데이터
  useEffect(() => {
    fetchPostData();
  }, [userToken]);

  // 포스트 데이터 가져오는 함수
  const fetchPostData = () => {
    fetch('/data/Postlist.json', {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${userToken}`,
      },
    })
      .then(response => {
        if (!response.ok) {
          throw new Error('네트워크 응답이 올바르지 않습니다');
        }
        return response.json();
      })
      .then(data => {
        if (Array.isArray(data.data)) {
          const sortedPosts = data.data.sort(
            (a, b) => new Date(b.createdAt) - new Date(a.createdAt),
          );
          setPostList(sortedPosts);
        } else {
          console.error('데이터가 배열이 아닙니다');
        }
      })
      .catch(error => {
        console.error(
          '데이터를 불러오는 중 오류가 발생했습니다:',
          error.message,
        );
        alert(error.message);
      });
  };

  // 로그인 후 이용가능 함수
  const checkAuth = () => {
    const userToken = localStorage.getItem('token');
    if (!userToken) {
      alert('로그인 후 이용할 수 있습니다.');
      return false;
    }
    return true;
  };

  // 로그인 후 포스트 작성 이용가능 함수
  const redirectToLoginPage = () => {
    const loginConfirmed = window.confirm(
      '로그인이 필요합니다. 로그인 하시겠습니까?',
    );
    if (loginConfirmed) {
      navigate('/');
    }
  };

  // 삭제 권한 로직 (삭제 버튼)
  const handleDelete = postId => {
    if (!checkAuth()) {
      return;
    }
    const deleteConfirmed = window.confirm('포스트를 삭제하시겠습니까?');
    if (deleteConfirmed) {
      fetch(`/data/Delete.json`, {
        method: 'DELETE',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${userToken}`,
        },
        body: JSON.stringify({
          postId: postId,
        }),
      })
        .then(response => {
          if (!response.ok) {
            if (response.status === 404) {
              throw new Error('포스트 삭제 과정에서 문제가 발생했습니다.');
            } else {
              throw new Error('포스트 삭제에 실패했습니다.');
            }
            return;
          }
          // 포스트가 성공적으로 삭제되면 새로운 리스트 받아오기 함수 실행
          fetchPostData();
        })
        .catch(error => {
          console.error('포스트 삭제 오류:', error.message);
          alert(error.message);
        });
    }
  };

  // 좋아요 로직
  const handlelike = postId => {
    if (!checkAuth()) {
      return;
    }
    fetch('/data/Postlike.json', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${userToken}`,
      },
      body: JSON.stringify({
        postId: postId,
      }),
    })
      .then(response => {
        if (!response.ok) {
          if (response.status === 404) {
            throw new Error('포스트 좋아요 처리 중 문제가 발생했습니다.');
          } else {
            throw new Error('서버 요청 오류');
          }
        }
        return response.json();
      })
      .then(data => {
        // 서버에서 받은 정보로 상태 업데이트
        fetchPostData();
        updateLike();
      })
      .catch(error => {
        console.error('Error:', error);
      });
  };

  // 서버 응답 이후에 상태를 업데이트하는 부분 수정
  const updateLike = (postId, isLiked) => {
    const updatedPostList = postList.map(post => {
      if (post.postId === postId) {
        return {
          ...post,
          isLiked: isLiked,
        };
      }
      return post;
    });
    setPostList(updatedPostList);
  };

  // 수정 권한 로직 (수정 버튼)
  const handleEdit = postId => {
    if (!checkAuth()) {
    } else {
      navigate(`/post-edit/${postId}`);
    }
  };

  // 포스트 작성 페이지 이동
  const handlePostAdd = () => {
    if (!checkAuth()) {
      redirectToLoginPage();
    } else {
      navigate('/post-add');
    }
  };

  // 댓글 페이지 이동
  const handleComment = postId => {
    if (!checkAuth()) {
    } else {
      navigate(`/comment/${postId}`);
    }
  };

  return (
    <div className="mainthread">
      <div className="scrollWrapper">
        {postList.map((post, index) => (
          <div className="postListFrame" key={index}>
            <div className="postList">
              <img
                className="profileImages"
                src={post.profileImage}
                alt="프로필 사진"
              />
              <span className="profileNameTexts">{post.nickname}</span>
              <span className="dateTexts">{post.createdAt}</span>
              {post.isUser && (
                <div>
                  <button
                    className="actionButtons deleteButton"
                    onClick={() => handleDelete(post.postId)}
                  >
                    삭제
                  </button>
                  <button
                    className="actionButtons editButton"
                    onClick={() => handleEdit(post.postId)}
                  >
                    수정
                  </button>
                </div>
              )}
            </div>
            <div className="postContentFrame">
              <p className="contentTexts">{post.content}</p>
              <div className="likeCommentFrame">
                <p className="likeTexts">좋아요 {post.likeCount}</p>
                <p
                  className="commentTexts"
                  onClick={() => handleComment(post.postId)}
                >
                  {post.comments}
                </p>
              </div>
              <img
                className="likeHearts"
                onClick={() => handlelike(post.postId, post.isLiked)}
                src={
                  post.isLiked ? '/images/likeHeart.svg' : '/images/heart.svg'
                }
                alt="좋아요"
              />
            </div>
          </div>
        ))}
      </div>
      <div className="footer">
        <div className="actionButtonFrame">
          <UserButton text="글 쓰기" onClick={handlePostAdd} />
        </div>
      </div>
    </div>
  );
};

export default MainThreadList;
  • 포스트 List 페이지는 최신순으로 작성된 post list 들을 보여주도록 구성
  • 글 쓰기 버튼을 누르면 유저 토큰의 유무를 확인후 포스트 작성 페이지로 이동
  • 삭제 버튼을 누르면 유저 토큰 및 로그인 유무 확인후 포스트 삭제
  • 수정 버튼을 누르면 유저 토큰 및 로그인 유무 확인후 포스트 수정 페이지로 이동

React 코드 - 로그인 후 이용가능 로직)

// 로그인 후 이용가능 함수
  const checkAuth = () => {
    const userToken = localStorage.getItem('token');
    if (!userToken) {
      alert('로그인 후 이용할 수 있습니다.');
      return false;
    }
    return true;
  };

  // 로그인 후 포스트 작성 이용가능 함수
  const redirectToLoginPage = () => {
    const loginConfirmed = window.confirm(
      '로그인이 필요합니다. 로그인 하시겠습니까?',
    );
    if (loginConfirmed) {
      navigate('/');
    }
  };
  • 유저 토큰이 없거나 로그인이 안된 상태에서 글 쓰기 버튼을 누르려고 하면 로그인 후 이용할 수 있도록 사용자에게 알림창으로 안내하고 확인버튼을 누르면 로그인 페이지로 이동할 수 있도록 구성

React 코드 - 권한 에러 로직)

// 삭제 권한 로직
... 생략 ...
	.then(response => {
          if (!response.ok) {
            if (response.status === 404) {
              throw new Error('포스트 삭제 과정에서 문제가 발생했습니다.');
            } else {
              throw new Error('포스트 삭제에 실패했습니다.');
            }
            return;
          }
          // 포스트가 성공적으로 삭제되면 새로운 리스트 받아오기 함수 실행
          fetchPostData();
        })
        .catch(error => {
          console.error('포스트 삭제 오류:', error.message);
          alert(error.message);
        });   
...

// 좋아요 권한 로직
... 생략 ...
	.then(response => {
        if (!response.ok) {
          if (response.status === 404) {
            throw new Error('포스트 좋아요 처리 중 문제가 발생했습니다.');
          } else {
            throw new Error('서버 요청 오류');
          }
        }
        return response.json();
      })
...
  • 불 특정 사용자가 로그인 하지 않은 상태에서 해당 포스트에 접근해 포스트를 마음대로 삭제하거나 수정하게 된다면 보안적인 면에서 큰 문제가 생기기 때문에, 이러한 피해를 막기 위해 fetch 메서드에 유저 토큰이 없거나 로그인이 안된 상태에서 자신이 작성한 포스트를 수정 또는 삭제, 좋아요를 누르려고 시도한다면 로그인후 이용해달라는 알림과 함께 권한 404 에러를 발생시켜 접근하지 못하도록 구성

메인 쓰레드 List - Mock Data)

{
  "data": [
    {
      "postId": 1,
      "nickname": "drawing_kim95",
      "profileImage": "https://img.jakpost.net/c/2020/08/26/2020_08_26_103148_1598423727._large.jpg",
      "content": "심심한데 그림 그리고 싶어...오늘은 뭐그릴까?",
      "createdAt": "2023-08-21",
      "updatedAt": "2023-08-22",
      "comments": "댓글 6",
      "isLiked": false,
      "likeCount": 0,
      "isUser": true
    },
    {
      "postId": 2,
      "nickname": "seongho",
      "profileImage": "https://hamtopia.com/web/product/tiny/202306/4990d9e0c64dcf0342ee120905977429.jpg",
      "content": "배고파 오늘은 뭐먹지?",
      "createdAt": "2023-08-20",
      "updatedAt": "2023-08-21",
      "comments": "댓글 5",
      "isLiked": false,
      "likeCount": 2,
      "isUser": false
    },
    {
      "postId": 3,
      "nickname": "wecode",
      "profileImage": "https://www.chemicalnews.co.kr/news/photo/202107/4159_10727_3352.png",
      "content": "일라이자 효과는 인간의 사고 과정과 감정을 AI 시스템에 잘못 돌리는 사람들의 경향을 말하며, 따라서 시스템이 실제보다 더 지능적이라고 믿는다. 이 현상은 1966년 MIT 교수 조셉 웨이젠바움이 만든 챗봇인 ELIZA의 이름을 따서 명명되었다.",
      "createdAt": "2023-08-19",
      "updatedAt": "2023-08-20",
      "comments": "댓글 3",
      "isLiked": false,
      "likeCount": 5,
      "isUser": false
    },
    {
      "postId": 4,
      "nickname": "guraeng",
      "profileImage": "https://img.freepik.com/premium-photo/fluffy-dwarf-hamster-lies-front-view_109543-618.jpg",
      "content": "일라이자 효과는 인간의 사고 과정과 감정을....",
      "createdAt": "2023-08-18",
      "updatedAt": "2023-08-19",
      "comments": "댓글 4",
      "isLiked": false,
      "likeCount": 3,
      "isUser": false
    },
    {
      "postId": 5,
      "nickname": "wecode_hong",
      "profileImage": "https://img.famtimes.co.kr/resources/2018/05/22/7J6ugOP2q6T7Z9Xy.jpg",
      "content": "성별과 성격과 같은 인간의 특성을 AI 음성 비서에게 돌리기",
      "createdAt": "2023-08-17",
      "updatedAt": "2023-08-18",
      "comments": "댓글 10",
      "isLiked": false,
      "likeCount": 7,
      "isUser": false
    },
    {
      "postId": 6,
      "nickname": "weread",
      "profileImage": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS7Xyj1ftWWW5ZjpyALJbMJ-mZwZLGAj_JLsTVAL2MKF7ULNCj7KGR5V6f4uJP8Msr7H3s&usqp=CAU",
      "content": "에러 체크 잘하기",
      "createdAt": "2023-08-16",
      "updatedAt": "2023-08-17",
      "comments": "댓글 20",
      "isLiked": false,
      "likeCount": 9,
      "isUser": false
    }
  ]
}

Fetch 메서드 - Mock Data 연결)

// 포스트 데이터
  useEffect(() => {
    fetchPostData();
  }, [userToken]);

  // 포스트 데이터 함수
  const fetchPostData = () => {
    fetch('/data/Postlist.json', {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${userToken}`,
      },
    })
      .then(response => {
        if (!response.ok) {
          throw new Error('네트워크 응답이 올바르지 않습니다');
        }
        return response.json();
      })
      .then(data => {
        if (Array.isArray(data.data)) {
          const sortedPosts = data.data.sort(
            (a, b) => new Date(b.createdAt) - new Date(a.createdAt),
          );
          setPostList(sortedPosts);
        } else {
          console.error('데이터가 배열이 아닙니다');
        }
      })
      .catch(error => {
        console.error(
          '데이터를 불러오는 중 오류가 발생했습니다:',
          error.message,
        );
        alert(error.message);
      });
  };
  • 해당 코드는 useEffect 안에서는 fetchPostData 함수가 호출되어 웹 페이지가 실행될때 랜더링 된 이후로 userToken 값이 변경될 때마다 호출되는데, 이는 userToken 값이 바뀔 때마다 새로운 데이터를 불러오는 방식이다. fetchPostData 함수는 /data/Postlist.json EndPoint로 GET 요청을 보내고, 해당 요청은 userToken을 포함한 헤더를 가지고 있으며, 백엔드 API로 부터 받은 응답을 확인하여 오류가 없으면 JSON으로 변환한 후에 데이터를 정렬해서 setPostList 함수를 통해 컴포넌트의 상태에 저장해서 포스트 리스트를 최신 날짜별로 나열해서 보여준다. 만약, 응답 데이터가 배열이 아닌 경우에는 콘솔에 에러를 출력하고, 네트워크 오류가 발생했을 때에도 오류 메시지를 출력하고 사용자에게 알림을 띄운다.

(6) 포스트 작성 페이지

React 코드 - 포스트 작성 페이지)

import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import './PostAdd.scss';

const PostAdd = () => {
  // 포스트 작성(저장)
  const [postContent, setPostContent] = useState('');
  // 유저 토큰, 닉네임, 프로필 이미지
  const userToken = localStorage.getItem('token');
  const nickName = localStorage.getItem('nickname');
  const profileImage = localStorage.getItem('profileImage');
  // 페이지 이동 하기
  const navigate = useNavigate();

  // 포스트 취소
  const handleCancel = () => {
    if (window.confirm('포스트 작성을 취소하시겠습니까?')) {
      window.alert('작성이 취소되었습니다.');
      navigate('/main-thread-list');
    }
  };

  // 작성한 포스트 등록하기
  const handlePost = () => {
    if (!postContent.trim()) {
      alert('포스트를 작성해주세요.');
      return;
    }

    const postConfirmed = window.confirm('포스트를 등록하시겠습니까?');
    if (postConfirmed) {
      fetch('/data/Create.json', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json;charset=utf-8',
          Authorization: `Bearer ${userToken}`,
        },
        body: JSON.stringify({
          content: postContent,
        }),
      })
        .then(response => {
          if (!response.ok) {
            throw new Error('네트워크 응답이 올바르지 않습니다');
          }
          return response.json();
        })
        .then(data => {
          if (data.message === 'UPLOAD SUCCESS') {
            alert('등록이 완료되었습니다.');
            navigate('/main-thread-list');
          } else {
            alert('등록에 실패했습니다. 다시 시도해주세요.');
          }
        });
    }
  };

  return (
    <div className="postAdd">
      <div className="profileWrapper">
        <div className="profileContainer">
          <img className="profileImages" src={profileImage} alt="프로필 사진" />
          <span className="profileText">{nickName}</span>
        </div>
        <div className="postInputContainer">
          <textarea
            className="postInput"
            placeholder="스레드를 시작하세요."
            value={postContent}
            onChange={event => setPostContent(event.target.value)}
          />
        </div>
        <div className="postButtonContainer">
          <button className="postButtons cancelButton" onClick={handleCancel}>
            취소
          </button>
          <button className="postButtons actionButton" onClick={handlePost}>
            게시
          </button>
        </div>
      </div>
    </div>
  );
};

export default PostAdd;
  • 포스트 작성 페이지는 리스트 페이지에서 글 쓰기 버튼을 누르면 로그인 유무를 확인 후, 작성 페이지로 이동되며 프로필 이미지와 닉네임을 가져와 표시할 수 있도록 구성
  • 리스트를 작성하지 않은 상태에서 게시 버튼을 누르면 한 글자 이상 입력해달라는 알림창을 띄워서 작성을 유도할 수 있도록 구현
  • 취소 버튼을 누르면 포스트 작성 취소여부를 확인할 수 있도록 알림창을 띄워서 사용자에게 보여주도록 구현

포스트 작성 Mock Data)

{
  "content": "유저가 작성 한 글"
}

Fetch 메서드 - Mock Data 연결)

	const postConfirmed = window.confirm('포스트를 등록하시겠습니까?');
    if (postConfirmed) {
      fetch('/data/Create.json', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json;charset=utf-8',
          Authorization: `Bearer ${userToken}`,
        },
        body: JSON.stringify({
          content: postContent,
        }),
      })
        .then(response => {
          if (!response.ok) {
            throw new Error('네트워크 응답이 올바르지 않습니다');
          }
          return response.json();
        })
        .then(data => {
          if (data.message === 'UPLOAD SUCCESS') {
            alert('등록이 완료되었습니다.');
            navigate('/main-thread-list');
          } else {
            alert('등록에 실패했습니다. 다시 시도해주세요.');
          }
        });
    }
  • 이 코드는 포스트를 서버에 등록하는 기능을 담당하며, 사용자가 등록을 원할 때 확인을 받은 후, fetch 함수를 사용하여 /data/Create.json 엔드포인트로 POST 요청을 보낸다. data 요청은 JSON 형식으로 이뤄지며, 서버로부터의 응답이 정상적이지 않은 경우 에러를 발생시키고, 응답이 정상적으로 온 경우에는 JSON으로 변환한 후 응답 데이터의 message를 확인하여 'UPLOAD SUCCESS'일 때 '등록이 완료되었습니다.' 알림을 표시한뒤 확인을 누르면 메인 쓰레드 페이지로 이동하게 된다. 만약 등록에 실패하면 '등록에 실패했습니다. 다시 시도해주세요.' 알림을 띄운다.

(7) 포스트 수정 페이지

React 코드 - 포스트 수정 페이지)

import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import './PostEdit.scss';

const PostEdit = () => {
  // 수정할 포스트 저장
  const [editContent, setEditContent] = useState('');
  // 유저 정보
  const userToken = localStorage.getItem('token');
  const nickName = localStorage.getItem('nickname');
  const profileImage = localStorage.getItem('profileImage');
  // postId 가져오기
  const { id: postId } = useParams();

  // 해당 postId의 포스트 데이터 가져오기
  useEffect(() => {
    fetch(`/data/Postlist.json?id=${postId}`, {
      method: 'GET',
      Authorization: `Bearer ${userToken}`,
    })
      .then(response => {
        if (!response.ok) {
          throw new Error('네트워크 응답이 올바르지 않습니다');
        }
        return response.json();
      })
      .then(data => {
        // 가져온 데이터에서 해당 postId의 내용을 가져와서 수정창에 표시
        if (data) {
          setEditContent(data.content);
        } else {
          throw new Error('포스트를 찾을 수 없습니다');
        }
      })
      .catch(error => {
        alert('포스트 데이터를 불러오던 중 오류가 발생했습니다.');
      });
  }, [postId, userToken]);

  // 페이지 이동 하기 : 취소
  const navigate = useNavigate();
  const handleCancel = () => {
    if (window.confirm('수정을 취소하시겠습니까?')) {
      window.alert('수정이 취소되었습니다.');
      navigate('/main-thread-list');
    }
  };

  // 포스트 수정 로직
  const handleEdit = postId => {
    if (!editContent.trim()) {
      alert('수정할 내용을 입력해주세요.');
      return;
    }

    const editConfirmed = window.confirm('게시글을 수정하시겠습니까?');
    if (editConfirmed) {
      fetch('/data/Update.json', {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${userToken}`,
        },
        body: JSON.stringify({
          content: editContent,
          postId: postId,
        }),
      })
        .then(response => {
          if (!response.ok) {
            if (response.status === 403) {
              throw new Error(
                '권한이 없습니다. 본인이 작성한 포스트만 수정할 수 있습니다.',
              );
            } else if (response.status === 404) {
              throw new Error(
                'CONTENT_NOT_FOUND: 해당 스레드를 찾을 수 없습니다.',
              );
            } else {
              throw new Error('네트워크 응답이 올바르지 않습니다');
            }
          }
          return response.json();
        })
        .then(data => {
          if (data.message === 'EDIT SUCCESS') {
            alert('수정이 완료되었습니다.');
            navigate('/main-thread-list');
          } else {
            alert('수정에 실패했습니다. 다시 시도해주세요.');
          }
        })
        .catch(error => {
          if (error.message.includes('권한이 없습니다')) {
            alert(error.message);
          } else if (error.message.includes('CONTENT_NOT_FOUND')) {
            alert(error.message);
          } else {
            alert(`오류가 발생했습니다: ${error.message}`);
          }
        });
    }
  };

  return (
    <div className="postEdit">
      <div className="profileWrapper">
        <div className="profileContainer">
          <img className="image" src={profileImage} alt="프로필 사진" />
          <span className="nickName">{nickName}</span>
        </div>
        <div className="editInputContainer">
          <textarea
            className="editInput"
            placeholder="내용 수정하기"
            value={editContent}
            onChange={e => setEditContent(e.target.value)}
          />
        </div>
        <div className="editButtonContainer">
          <button className="editButtons cancelButton" onClick={handleCancel}>
            취소
          </button>
          <button className="editButtons actionButton" onClick={handleEdit}>
            게시
          </button>
        </div>
      </div>
    </div>
  );
};

export default PostEdit;
  • 리스트 작성 페이지에서 로그인 된 유저에 한해서만 수정 버튼을 눌렀을때 수정 페이지로 이동할수 있도록 구성
  • 작성 페이지와 마찬가지로 게시 버튼을 누르면 수정할 내용에 대한 알림을 띄워 수정한 내용을 반영할 것인지에 대해 알려주도록 구현
  • 수정을 취소하고자 할경우 수정 버튼을 누르면 포스트 수정을 취소 할것인지에 대한 내용을 알림창을 띄워서 사용자에게 보여주도록 구현

포스트 수정 Mock Data)

{
  "postId": 1,
  "content": "수정한 게시글의 내용"
}

Fetch 메서드 - Mock Data 연결)

	const editConfirmed = window.confirm('게시글을 수정하시겠습니까?');
    if (editConfirmed) {
      fetch('/data/Update.json', {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${userToken}`,
        },
        body: JSON.stringify({
          content: editContent,
          postId: postId,
        }),
      })
        .then(response => {
          if (!response.ok) {
            if (response.status === 403) {
              throw new Error(
                '권한이 없습니다. 본인이 작성한 포스트만 수정할 수 있습니다.',
              );
            } else if (response.status === 404) {
              throw new Error(
                'CONTENT_NOT_FOUND: 해당 스레드를 찾을 수 없습니다.',
              );
            } else {
              throw new Error('네트워크 응답이 올바르지 않습니다');
            }
          }
          return response.json();
        })
        .then(data => {
          if (data.message === 'EDIT SUCCESS') {
            alert('수정이 완료되었습니다.');
            navigate('/main-thread-list');
          } else {
            alert('수정에 실패했습니다. 다시 시도해주세요.');
          }
        })
        .catch(error => {
          if (error.message.includes('권한이 없습니다')) {
            alert(error.message);
          } else if (error.message.includes('CONTENT_NOT_FOUND')) {
            alert(error.message);
          } else {
            alert(`오류가 발생했습니다: ${error.message}`);
          }
        });
    }
  };
  • 이 코드는 사용자가 게시글을 수정하려고 할 때 동작하는 기능을 담당하는 fetch 메서드이다. 서버로부터의 응답이 정상적이지 않은 경우에는 각각의 상황에 따라 다른 에러 메시지를 생성하여 처리하는데 예를 들어, 권한이 없는 경우 403 Forbidden 에러가 발생하고, 해당 에러에 대한 메시지를 사용자에게 알려줍니다. 또한, 찾을 수 없는 컨텐츠에 대해서도 404 Not Found 에러가 발생하며, 이에 대한 메시지도 사용자에게 전달되는 방식으로 구현되어 있다.

(8) 댓글 작성 페이지

React 코드 - 댓글 작성 페이지)

import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import './Comment.scss';
import UserInput from '../../Components/UserInput';

const Comment = () => {
  // 댓글 리스트 관리
  const [commentList, setCommentList] = useState([]);
  // 유저 토큰
  const userToken = localStorage.getItem('token');
  // 유저 프로필, 닉네임
  const nickName = localStorage.getItem('nickname');
  const profileImage = localStorage.getItem('profileImage');
  // postId 가져오기
  const { id: postId } = useParams();
  // 페이지 이동
  const navigate = useNavigate();

  // 댓글 리스트 데이터
  useEffect(() => {
    fetch(`/data/Commentlist.json?id=${postId}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${userToken}`,
      },
    })
      .then(response => {
        if (!response.ok) {
          throw new Error('네트워크 응답이 올바르지 않습니다');
        }
        return response.json();
      })
      .then(data => {
        if (Array.isArray(data.data)) {
          const sortedPosts = data.data.sort(
            (a, b) => new Date(b.createdAt) - new Date(a.createdAt),
          );
          setCommentList(sortedPosts);
        } else {
          console.error('데이터가 배열이 아닙니다');
        }
      })
      .catch(error => {
        console.error(
          '데이터를 불러오는 중 오류가 발생했습니다:',
          error.message,
        );
        alert(error.message);
      });
  }, [userToken]);

  // 메인 스레드 페이지 이동
  const handleBack = () => {
    if (window.confirm('댓글 작성을 취소하시겠습니까?')) {
      window.alert('댓글 작성이 취소되었습니다.');
      navigate('/main-thread-list');
    }
  };

  return (
    <div className="comment">
      <div className="commentWrapper">
        <div className="backButtonContainer">
          <img
            className="backButtonIcon"
            src="/images/Back_arrow.svg"
            alt="뒤로 아이콘"
          />
          <button className="backButton" onClick={handleBack}>
            뒤로
          </button>
        </div>
        <div className="userPostContainer">
          <img className="profileImages" src={profileImage} />
          <span className="profileNames">{nickName}</span>
          <span className="dayTexts">2023-08-21</span>
        </div>
        <div className="mainPostFrame">
          <span className="postText">
            심심한데 그림 그리고 싶어...오늘은 뭐그릴까?
          </span>
          <p className="postCommentText">댓글 6</p>
        </div>
        <div className="commentActionsFrame">
          <UserInput type="text" placeholder="댓글을 작성해주세요." />
          <button className="actionButton">댓글 개시</button>
        </div>
        <div className="listContainer">
          {commentList.map((comment, index) => (
            <div className="commentListFrame" key={index}>
              <img
                className="profileImages"
                src={comment.profileImage}
                alt="프로필 사진"
              />
              <div className="userInfo">
                <span className="profileNames">{comment.nickname}</span>
                <p className="commentText">{comment.comment}</p>
              </div>
              <div className="dayUpdates">
                <span className="updateText">{comment.updatedAt}</span>
                {comment.isUser && (
                  <div>
                    <button className="actionButtons deleteButton">삭제</button>
                    <button className="actionButtons editButton">수정</button>
                  </div>
                )}
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

export default Comment;
  • 댓글 작성 페이지는 로그인한 유저에 한해 댓글을 작성할수 있도록 입력창과 버튼으로 구성했고, 댓글 리스트는 Mock Data를 이용해 최신순으로 List들을 나열
  • 댓글 List 들은 스크롤바를 이용해서 밑에 보이지 않는 댓글들을 확인 가능

댓글 List Mock Data)

{
  "data": [
    {
      "postId": 1,
      "nickname": "drawing_kim95",
      "profileImage": "/images/profileImage_03.jpg",
      "comment": "오늘은 너가 그리고 싶은거 위주로 그려봐!",
      "updatedAt": "2023.08.21",
      "isUser": true
    },
    {
      "postId": 2,
      "nickname": "seongho",
      "profileImage": "/images/profileImage_01.jpg",
      "comment": "작가님! 오늘은 저 그려주세요!",
      "updatedAt": "2023.08.22",
      "isUser": false
    },
    {
      "postId": 3,
      "nickname": "wecode",
      "profileImage": "/images/profileImage_02.jpg",
      "comment": "이번엔 음식을 한번 그려보는건 어때?",
      "updatedAt": "2023.08.23",
      "isUser": false
    },
    {
      "postId": 4,
      "nickname": "guraeng",
      "profileImage": "/images/profileImage_04.jpg",
      "comment": "내 마스코트 그려주세요! 비용 드릴께요!",
      "updatedAt": "2023.08.24",
      "isUser": false
    },
    {
      "postId": 5,
      "nickname": "wecode_hong",
      "profileImage": "/images/profileImage_05.jpg",
      "comment": "저번에 전시회 준비하는거 같던데 전시회 한번 준비해보셔야죠!",
      "updatedAt": "2023.08.25",
      "isUser": false
    },
    {
      "postId": 6,
      "nickname": "wecode_lee",
      "profileImage": "/images/profileImage_06.jpg",
      "comment": "그림 작가시니 그림과 관련된 프로젝트 하나 만들어 보시죠!",
      "updatedAt": "2023.08.26",
      "isUser": false
    },
    {
      "postId": 7,
      "nickname": "shk_9505",
      "profileImage": "/images/profileImage_03.jpg",
      "comment": "같이 여행가면서 그림이나 그리자 ㅋㅋ 그림 챙기고 얼렁 나오셈!",
      "updatedAt": "2023.08.27",
      "isUser": false
    },
    {
      "postId": 8,
      "nickname": "juyoung_0211",
      "profileImage": "/images/profileImage_05.jpg",
      "comment": "우왕..대단해!",
      "updatedAt": "2023.08.28",
      "isUser": false
    }
  ]
}

Fetch 메서드 - Mock Data 연결)

// 댓글 리스트 데이터
  useEffect(() => {
    fetch(`/data/Commentlist.json?id=${postId}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${userToken}`,
      },
    })
      .then(response => {
        if (!response.ok) {
          throw new Error('네트워크 응답이 올바르지 않습니다');
        }
        return response.json();
      })
      .then(data => {
        if (Array.isArray(data.data)) {
          const sortedPosts = data.data.sort(
            (a, b) => new Date(b.createdAt) - new Date(a.createdAt),
          );
          setCommentList(sortedPosts);
        } else {
          console.error('데이터가 배열이 아닙니다');
        }
      })
      .catch(error => {
        console.error(
          '데이터를 불러오는 중 오류가 발생했습니다:',
          error.message,
        );
        alert(error.message);
      });
  }, [userToken]);
  • 이 코드는 특정 포스트에 대한 댓글 리스트를 가져오는 역할을 한다. useEffect를 사용해서 컴포넌트가 렌더링될 때마다 해당 댓글 리스트를 가져오도록 설정되어 있고, 만약, 네트워크 오류가 발생했을 때에도 오류 메시지를 출력해서 사용자에게 알림을 띄우게 된다.

🚨 기술적 문제 & 개선 성과

[기술 문제]

  • 불 특정 사용자가 로그인을 하지 않은 상태에서 포스트 작성 및 수정 하려고 시도할 때, 유저 권한 에러가 발생하자 않아, 사용자의 정보를 마음대로 수정하거나 삭제할 수 있는 윤리적 문제가 발생할 기술 문제의 위험성을 감지하였습니다.

[고민]

  • 이러한 기술적인 문제에 대응하기 위해서 로그인하지 않은 사용자는 포스트 이용 권한을 제한하는 방법을 선택해서 윤리적 문제를 해결하고, 사용자들에게 안전한 커뮤니티 경험을 제공해야 할 필요성을 느끼게 되었습니다.

[시도 방법]

  • 로그인하지 않은 불 특정 사용자가 다른 유저 포스트에 좋아요를 누르거나 수정 또는 삭제하려고 시도할 때, fetch 메서드를 활용해 localstorge에 유저 토큰의 유무를 확인 및 404 권한 에러를 발생시키도록 기능을 수정하여, 로그인이 안된 상태에서도 포스트를 작성하거나 수정 또는 삭제 할 경우에는, 로그인 후 이용해달라는 알림 문구를 띄워서 문제를 보완했습니다.

[개선 성과]

  • 유저 권한 에러를 통해 로그인하지 않은 사용자들이 다른 사용자의 정보를 수정하거나 삭제하는 시도를 차단하여, 사용자들이 안전하고 신뢰할 수 있는 커뮤니티를 경험할 수 있도록 보완하였습니다. 이를 통해 사용자들이 안심하고 커뮤니티를 이용할 수 있도록 개선했습니다.

🧑🏻‍💻 가장 기억에 남는 코드

1) 가장 뿌듯했다고 느꼈던 코드

// 포스트 데이터
  useEffect(() => {
    fetchPostData();
  }, [userToken]);

  // 포스트 데이터 가져오는 함수
  const fetchPostData = () => {
    fetch('/data/Postlist.json', {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${userToken}`,
      },
    })
      .then(response => {
        if (!response.ok) {
          throw new Error('네트워크 응답이 올바르지 않습니다');
        }
        return response.json();
      })
      .then(data => {
        if (Array.isArray(data.data)) {
          const sortedPosts = data.data.sort(
            (a, b) => new Date(b.createdAt) - new Date(a.createdAt),
          );
          setPostList(sortedPosts);
        } else {
          console.error('데이터가 배열이 아닙니다');
        }
      })
      .catch(error => {
        console.error(
          '데이터를 불러오는 중 오류가 발생했습니다:',
          error.message,
        );
        alert(error.message);
      });
  };
  • 이 코드는 fetch와 Mock Data를 활용한 포스트 리스트를 뽑아내는 로직으로, 이 코드가 가장 기억에 남았던 이유는 실제 백엔드 API와 맞추기 전에 Mock Data를 활용하여 포스트별로 리스트를 나열해서 보여주는지 원리를 이해하며 활용할 수 있었기 때문에 프로젝트 수행시 가장 인상깊게 남았습니다.

2) 아쉬웠던 코드

... 생략 ...

// 좋아요 로직
  const handlelike = postId => {
    if (!checkAuth()) {
      return;
    }
    fetch('/data/Postlike.json', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${userToken}`,
      },
      body: JSON.stringify({
        postId: postId,
      }),
    })
      .then(response => {
        if (!response.ok) {
          if (response.status === 404) {
            throw new Error('포스트 좋아요 처리 중 문제가 발생했습니다.');
          } else {
            throw new Error('서버 요청 오류');
          }
        }
        return response.json();
      })
      .then(data => {
        // 서버에서 받은 정보로 상태 업데이트
        fetchPostData();
        updateLike();
      })
      .catch(error => {
        console.error('Error:', error);
      });
  };

  // 서버 응답 이후에 상태를 업데이트하는 부분 수정
  const updateLike = (postId, isLiked) => {
    const updatedPostList = postList.map(post => {
      if (post.postId === postId) {
        return {
          ...post,
          isLiked: isLiked,
        };
      }
      return post;
    });
    setPostList(updatedPostList);
  };
  
  ... 생략 ...
  
  <div className="likeCommentFrame">
     <p className="likeTexts">좋아요 {post.likeCount}</p>
        <p
          className="commentTexts"
          onClick={() => handleComment(post.postId)}
        >
          {post.comments}
        </p>
  </div>
     <img
        className="likeHearts"
        onClick={() => handlelike(post.postId, post.isLiked)}
        src={post.isLiked ? '/images/likeHeart.svg' : '/images/heart.svg'}
        alt="좋아요"
     />
     
 ... 생략 ...
  • 이 코드는 좋아요 기능을 fetch 메서드를 활용하여 만든 로직으로 이 코드가 가장 아쉽게 남았던 이유는 시간 관계상 만들지 못했던 부분도 있었지만 처음으로 fetch랑 연결해서 구현하는게 처음이다보니 혼자서 구현하기엔 한계점이 너무 많아서 결국엔 구현하지 못하고 끝나버렸기 때문에, 좋아요 기능에 대한 아쉬움이 너무 많았던 코드로 남았습니다.

💻 Wereads 프로젝트 회고

wereads 프로젝트를 끝내고 9월부터 지금까지 순간들을 돌아봤을때, 지금 이 회고록을 작성하는 순간에도 그리고 개발자로써 필요한 마인드는 무엇일까?에 대해서 스스로 생각할 수 있는 시간을 가지면서 프로젝트에 임했던거 같아요. 특히 멘토님의 빡센 코드리뷰를 어떻게 빠르게 적용해야 하는지 고민이 많았는데 코드리뷰 덕분에 코드를 어떻게 적용해야 좋은 코드이고 지저분한 스파게티 코드 한줄을 어떻게 줄여볼 수 있을까를 생각하는 습관을 가지게 되면서 개발자는 끊임없는 도전과 성장으로 업그레이드 된다는걸 체감했습니다. 먼저, 9월부터 지금 현재 순간을 돌아보며 성장했다고 느꼈던 점을 회고하면 다음과 같습니다.

뿌듯했던 점)

  • 1번째, 팀 프로젝트 수행시 분배된 업무를 성실하게 수행하기위해 노력한 점
  • 2번째, 이슈 사항 이외에 공유사항을 즉각적으로 공유하려고 노력했고 모르는 내용은 팀원과 멘토님께 질문하며 자신이 가지고 있는 어려움을 깨트리려고 노력했던 점
  • 3번째, 멘토님의 코드리뷰를 통해 올바른 방향성을 이해하고 스스로 돌아본 점
  • 4번째, Git, Github의 기본 사용 방법, 그리고 React에서의 Mock Data 활용성을 배운 점
  • 5번째, 공용 컴포넌트를 prop으로 재사용함과 동시에 적용하는 방법을 알게 된점
  • 6번째, 프로젝트를 수행하며 사용자의 관점에서 이해하며 제품을 개발하는 것이 얼마나 중요한지 느꼈던 점
  • 7번째, CRA, ESLint, Prettier 셋팅, 각 페이지별 기능 구현, Github 배포까지 모든 과정을 수행하면서 독립적인 역량을 강화할 수 있었던 점

아쉬웠던 점)

  • 백엔드 API 담당 멘토님의 소통 부재로 인해 일부 기능이 미완성으로 마무리되었다는 점... 페이지 별 API Test를 위해 멘토님과 날짜 협의를 시도했지만, 소통이 원활하지 않았던 문제로 개발 과정의 마지막에 예상치 못한 어려움에 직면하게 되었고, 프로젝트 일정에 영향을 미쳤다는게 아쉬웠어요...멘토님과의 원활한 소통이 이루어지지 않았던 경험을 통해, 개발자로서 프로젝트 참여자 간 효과적인 소통과 팀 협업의 중요성을 몸소 깨닫게 되었습니다.
    (프로젝트 기간이 종료되었더라도 추후에, 커뮤니티 프로젝트를 진행하거나, 유지보수를 진행하면서 부족한 내용에 대해 끊임없이 학습하며 적용하고 싶습니다.)

이렇게 프로젝트를 통해 뿌듯했던 점과, 아쉬웠던 점을 회고할 수 있음에 감사합니다😄 추후에 아쉽게 느꼈던 점들이 하나의 장점으로 현업에서도 적용하기 위해 노력한다면, 이 아쉬움도 장점으로 만들 수 있다고 생각했어요:) 제 자신 스스로에게 적용하는 방법을 알아가는 과정이라서 그런거지, 제 자신이 애초에 못한다고 생각하지 않거든요:)

이제 시작입니다. 위코드를 졸업하고 나서, 본격적인 개발자의 꿈을 향해 나아갈 준비를 하고 있지만, 생각해보면 저는 과연 어떤 개발자인지 생각을 하지 못했는데, 결론적으론 아직까지 부족한 실력이더라도 개발에 열정이 있고 진심으로 행동하기 위해 노력하는 사람이라는걸 스스로 돌아보게 되었습니다. 그렇다고 완벽하게 잘하는 개발자는 아니예요! ㅋㅋㅋㅋ 제 목표는 저보다 어려운 삶을 개선시키는 개발자가 되는게 사명이자 목표니까요 그 꿈을 포기 하지않고 나아가는 저와 응원해주시는 모든 분들께 다시한번 감사하다는 말씀 드리고 싶습니다:)

그동안 수고 많았어요:)
다음도 차근차근 준비해봅시다!!

Fighting!

profile
안녕하세요 Junior UIUX Designer 입니다 😊

0개의 댓글