[WIL 7주차] 항해 99 팀프로젝트 페이스북 클론코딩 with 리액트

hoonie·2021년 7월 25일
0

WIL

목록 보기
7/7
post-thumbnail

이번 주차는 클론코딩을 해보는 주차였다.
어떠한 클론코딩을 해야지 실용적인 많은 기능들을 학습할 수 있을까 생각하다가

crud를 다 사용하고 좋아요, 친구추가 등 많은 유용한 기능들이 있는 페이스북으로 선정을 하였다.


프로젝트 설명

프로젝트 기간 : 2021-07-19 ~ 2012-07-22

프로젝트명 : 페이스북 클론코딩

팀 구성 : 리액트 개발자 2명, 스프링부트 개발자 2명

사용 기술 : 리액트, 파이어베이스, 스프링부트

사용 IDE : VS Code

프로젝트 설명 : SNS로 유명한 페이스북의 클론코딩

기능 : 회원가입 및 로그인, 프로필 및 커버 이미지 업로드, 게시글 등록, 수정 및 삭제, 게시글 좋아요, 친구요청등

프로젝트 진행 순서 : 프로젝트 클론 코딩 대상 선정 -> 필요한 기능 선정 -> 필요한 api 별 정리(url 및 파라미터명) -> 프론트엔드 및 백엔드 개발자들 역할 분담 -> 기능 구현 -> 기능 테스트

구현된 api swagger - http://13.124.141.66/swagger-ui/index.html


프로젝트 결과물

로그인 및 회원가입

  1. 유저의 정보들을 받아와 signup post api에 전송후 db에 저장

// 회원가입 API
const signupAPI = (username, password, passwordChecker, emailAddress) => {
  return function (dispatch, getState, { history }) {
    const user = { username, password, passwordChecker, emailAddress };
    instance.post("user/signup", user).then((result) => {
      history.push("/login");
      window.alert("회원가입 완료. 환영합니다!");
    });
  };
};

코드설명

  • 필요한 유저정보를 state에 담아 가입하기 버튼 클릭시 signupAPI 함수 실행 -> 해당 api에 파라미터 전송 후 디비저장 -> 회원가입 완료'
  1. 가입했을때 입력한 이메일 주소와, 비밀번호를 입력받아 로그인

// 로그인 API
const loginAPI = (emailAddress, password) => {
  return function (dispatch, getState, { history }) {
    const user_login = { emailAddress, password };
    instance
      .post("user/login", user_login)
      .then((result) => {
        const accessToken = result.data; // API 요청하는 콜마다 해더에 accessTocken 담아 보내도록 설정
        instance.defaults.headers.common["Authorization"] = `${accessToken}`;
        setCookie("token", accessToken, 1, "/");
        var decoded = jwt_decode(accessToken);
        dispatch(
          logIn({
            username: decoded.sub,
          })
        );
        history.push("/");
      })
      .catch((error) => {
        console.log(error);
        window.alert("로그인 실패");
      });
  };
};

코드설명

  • 이메일주소와 비밀번호를 상태값에 넣음 -> 로그인 버튼 클릭시 loginAPI 함수 실행 -> api에 로그인 요청 보낸 후 accessToken 받아옴 -> axios 기본 headers Authorization 권한 값에 토큰 저장 -> 해당 토큰 쿠키에 set 하기 -> 토큰 decoding하여 토큰에 있는 유저 정보를 리덕스 user 상태값에 넣기 -> 로그인 성공시 메인페이지로 이동

3.loginCheck 함수로 현재 로그인 상태 유무 확인(새로고침하면 redux상태값이 초기화가 되므로 방지하기 위하여 App.js에 loginCheck() 넣기)


const loginCheck = () => {
  return function (dispatch, getState, { history }) {
    if (getCookie("token")) {
      const token = getCookie("token");
      var decoded = jwt_decode(token);
      instance.defaults.headers.common["Authorization"] = `${token}`;
      dispatch(
        logIn({
          username: decoded.sub,
        })
      );
    }
  };
};

코드설명

  • 로그인 했을때 설정했던 token 쿠키값을 받아오기 -> 만약 token이 존재하면 decoding 후 해당 user 정보를 user redux 상태값에 저장
  1. 로그아웃 기능

// 로그아웃 API
const logOutAPI = () => {
  return function (dispatch, getState, { history }) {
    deleteCookie("token");
    instance.defaults.headers.common["Authorization"] = null;
    delete instance.defaults.headers.common["Authorization"];
    dispatch(logOut());
    history.replace("/login");
  };
};

코드설명

  • 로그아웃 버튼 클릭시 해당 토큰을 가진 쿠키 및 axios 헤더 기본값 제거

개인 상세페이지

  1. firebase를 이용하여 이미지 업로드시 url를 받아오고 프로필 디비에 저장

  const selectProfileFile = (e) => {
    const reader = new FileReader();
    const file = profileInput.current.files[0];
    reader.readAsDataURL(file);
    reader.onloadend = () => {
      dispatch(uploadAction.uploadProfileImg(file));
    };
  };

const uploadProfileImg = (file) => {
  return function (dispatch, getState, { history }) {
    dispatch(actionLoading.setLoading(true));

    let _upload = storage.ref(`images/${file.name}`).put(file);
    let username = getState().user.user.username;

    _upload.then((snapshot) => {
      snapshot.ref.getDownloadURL().then((url) => {
        const param = { picture: url, username };
        instance.put("user/userprofile/picture", param).then((result) => {
        
        //내 프로필 이미지 리덕스 상태값에 url 넣기
          dispatch(profileAction.getProfileImage(url));
          
          //loading 컴포넌트 안보이게
          dispatch(actionLoading.setLoading(false));
          
          //개인상세페이지에 내가 등록한 포스트 리스트 뿌려주기
          dispatch(postAction.getMyPostDB());
        });
      });
    });
  };
};

코드설명

  • 프로필 업로드할때 쓰이는 input type file change시 selectProfileFile 함수 실행 -> 해당 파일값 담아서 uploadProfileImg 액션함수 dispatch -> firebase storage에 업로드 -> 업로드 완료 후 url 받아오기 -> url 디비에 저장 -> 내 프로필 리덕스 상태값에 url setting 하기 -> 해당 리덕스 상태값에서 url 받아와서 프로필 이미지 src에 뿌리기

친구 검색 페이지

  1. 검색한 키워드를 가지고 있는 유저이름들 출력

  const handleSearch = () => {
    const inputValue = inputRef.current.value;
    if (inputValue === "") {
      dispatch(searchAction.deleteSearchListAll());
    } else {
      dispatch(searchAction.getSearchListDB(inputValue));
    }
    setCurrentValue(inputValue);
  };

const getSearchListDB = (friendName) => {
  return function (dispatch, getState, { history }) {
    const username = getState().user.user.username;
    instance
      .get(`/user/search/contain-list/${username}/${friendName}`)
      .then((result) => {
        dispatch(getSearchList(result.data));
      });
  };
};

  const keyUpHandler = (currentValue) => {
    const searchedPara = document.querySelectorAll(".text");
    if (searchedPara.length > 0) {
      const words = currentValue;
      const regex = RegExp(words, "gi");
      const replacement = "<strong style=color:#3b5998>" + words + "</strong>";

      for (let i = 0; i < searchedPara.length; i++) {
        const newHTML = searchedPara[i].textContent.replace(regex, replacement);
        searchedPara[i].innerHTML = newHTML;
      }
    }
  };

const searchedWord = useSelector((state) => state.search.search_list);

  useEffect(() => {
    keyUpHandler(currentValue);
  }, [searchedWord]);

코드설명

  • 검색어 입력 input에 글자 입력시 handleSearch 함수 실행 -> 해당 inputValue값을 넘겨서 getSearchListDB 액션함수 dispatch 하기 -> 로그인한 유저의 username 리덕스 상태값과 입력했던 inputValue 값을 보내서 해당되는 유저의 리스트들 받아오기 -> getSearchList 액션함수 이용하여 search.search_list 리덕스 상태값에 배열형태로 저장 -> 저장된 list 상태값을 받아서 searchedWord 라는 변수에 할당 -> 해당 변수에 이벤트가 발생할때마다 실행되도록 useEffect 안에 keyUpHandler라는 함수 실행하여 현재 입력된 currentValue값을 넣어 입력된 그 값에 파란글씨 적용

친구요청

  1. 검색된 유저목록에서 사람 아이콘 클릭시 친구 요청 및 요청 취소

  const handleRequestFriend = () => {
    dispatch(friendAction.requestFriendDB(requestParam));
  };

  const handleCancleRequestFriend = () => {
    dispatch(friendAction.requestCancleFriendDB(requestParam));
  };

const requestFriendDB = (friend) => {
  return function (dispatch, getState, { history }) {
    instance.post("user/request-friend", friend).then((result) => {
      const { friendName } = friend;
      dispatch(
        searchAction.getSearchDetailListDB(friendName.replace(/[0-9]/g, ""))
      );
    });
  };
};

const requestCancleFriendDB = (friend) => {
  return function (dispatch, getState, { history }) {
    const { username, friendName } = friend;
    instance
      .delete(`user/decline-friend/given/${username}/${friendName}`)
      .then((result) => {
        dispatch(
          searchAction.getSearchDetailListDB(friendName.replace(/[0-9]/g, ""))
        );
      });
  };
};

코드설명

  • 친구 요청 아이콘 클릭시 handleRequestFriend 함수 실행 -> 친구의 이름을 변수로 받아 user/request-friend api 실행하여 db에 친구 요청 기록 저장 -> 다시 친구 이름 목록을 호출하는 getSearchDetailListDB호출(바뀐 디비를 다시 호출하여 친구요청 아이콘이 검은색으로 바뀌도록)

1.자신한테 친구 요청들어온 친구 요청 목록 가져오기


const requestedFriendListDB = () => {
  return function (dispatch, getState, { history }) {
    let username = getState().user.user.username;
    instance
      .get(`/user/request-friend-list/received/${username}`)
      .then((result) => {
        dispatch(getRequestedFriendList(result.data));
      });
  };
};

코드설명

  • 친구 요청 페이지 렌더링시 requestedFriendListDB 실행 -> 자신한테 요청들어온 친구들의 목록을 응답받아오기 -> 리덕스 상태값에 저장 -> 저장된 상태값 목록으로 뿌리기
  1. 친구 수락 후 내 친구 목록으로 뿌리기

const acceptRequestedFriend = (friendName) => {
  return function (dispatch, getState, { history }) {
    let username = getState().user.user.username;
    const param = {
      username,
      friendName,
    };
    instance.post("/user/accept-friend", param).then(() => {
      dispatch(requestedFriendListDB());
    });
  };
};

const getMyFriendListDB = () => {
  return function (dispatch, getState, { history }) {
    let username = getState().user.user.username;
    instance.get(`user/friends/${username}`).then((result) => {
      dispatch(getMyFriendList(result.data.friends));
    });
  };
};

코드설명

  • 요청 들어온 친구목록에서 확인 버튼 클릭시 acceptRequestedFriend 함수 실행 -> 친구 수락이 완료 되면 다시 requestedFriendListDB를 실행하여 업데이트 된 화면 노출 -> getMyFriendListDB 를 실행하여 수락한 친구를 내 친구 목록으로 넣기

게시글 작성

  1. 입력한 contents 내용과 등록한 사진리스트들을 담아서 디비에 저장
const selectImgFile = (e, type) => {
    const reader = new FileReader();
    const file = imageInput.current.files[0];
    reader.readAsDataURL(file);

    reader.onloadend = () => {
      const param = { file: file, preview: reader.result };
      dispatch(previewActions.setImagePreview(param));
    };
  };
export default handleActions(
  {
    [SET_IMAGE_PREVIEW]: (state, action) =>
      produce(state, (draft) => {
        draft.images.push(action.payload.image);
      }),
  },
  initialState
);

  const imagePreview = useSelector((state) => state.preview.images);

코드설명

  • input type file change 시 selectImgFile 함수 실행 -> 해당 파일의 reader.result 값을 받아 setImagePreview 액션함수 디스패치 실행 -> preview 리덕스 내 images 상태값에 미리보기 등록한 사진들 계속 push -> 푸시 완료된 imagePreview 리스트들 map으로 출력하기

const uploadImageFB = (type) => {
  return function (dispatch, getState, { history }) {
    const preview = getState().preview;
    let article = getState().article.article;
    const imageLength = preview.images.length;
    const videoLength = preview.videos.length;
    dispatch(actionLoading.setLoading(true));

    if (imageLength > 0 || videoLength > 0) {
      for (let key in preview) {
        preview[key].map(({ file }) => {
          let _upload;
          if (key === "images") {
            _upload = storage.ref(`images/${file.name}`).put(file);
          } else if (key === "videos") {
            _upload = storage.ref(`videos/${file.name}`).put(file);
          }
          _upload.then((snapshot) => {
            snapshot.ref.getDownloadURL().then((url) => {
              if (key === "images") {
                dispatch(previewAction.deleteAllImagePreview());

                dispatch(setUploadImageUrlList(url));
              } else if (key === "videos") {
                dispatch(setUploadVideoUrlList(url));
              }
            });
          });
        });
      }
      setTimeout(() => {
        if (type === "add") {
          dispatch(articleAction.addArticleDB(article));
        } else {
          dispatch(articleAction.updateArticleDB(article));
        }
      }, 4000);
    } else {
      if (type === "add") {
        dispatch(articleAction.addArticleDB(article));
      } else {
        dispatch(articleAction.updateArticleDB(article));
      }
    }
  };
};


const addArticleDB = (article) => {
  return function (dispatch, getState, { history }) {
    const picture = getState().upload.upload_img_url;
    const video = getState().upload.upload_video_url;

    const pictureParam = picture.join(',');
    const videoParam = video.join(',');

    const param = {
      ...article,
      picture: pictureParam,
      video: videoParam,
    };

    instance
      .post(`/user/article`, param)
      .then((result) => {
        dispatch(actionLoading.setLoading(false));
        history.replace('/');
      })
      .catch((error) => {
        console.log('error : ', error);
      });
  };
};

코드설명

  • 게시글 내용하고 원하는 사진을 등록 후 버튼 클릭시 uploadImageFB 함수 실행 -> preview 사진 리스트들 for문으로 반복문 돌려 모든 파일 firebase storage 에 저장 -> 저장된 이미지 url 받아오기 -> 받아온 url과 필요한 게시글 state 정보들 받아 addArticleDB 실행 -> 게시글 등록 완료 ( type이 업데이트라면 update해주는 함수 실행되도록 조건문을 걸어줌 )

등록된 게시글 출력


느낀점

  • 이번 페이스북 클론 코딩에서 정말 제대로된 페이스북 처럼 따라 만들어보고자하는 욕심이 컸었다. 가능하면 소켓을 활용한 채팅기능도 만들어보고 싶었고, 실시간 데이터베이스를 이용하여 알람기능도 만들어보고 싶었다. 하지만 생각보다 개발속도가 더디게 되어서 비록 기간내에 완성은 못하였지만, 추후에는 꼭 저 기능들을 다 넣어서 완성본을 만들어보고 싶다는 생각이 들었다. 클론코딩의 레이아웃 같은 경우는 해당 웹서비스의 css를 그대로 이용하면 돼서 빠르게 뷰를 잡을 수 있을 것이라고 생각했지만, 의외로 정상적으로 작동이 안할때도 꽤 있었고, 너무 같은 css 코드를 따라하려니 오히려 시간이 더 많이 걸리는 느낌이라, 그냥 구조를 보고 내 판단대로 다시 마크업하고 css를 짜게되었다. 이러한 과정이 html과 css 역량 또한 높이는 길이라 보람있고, 좋은 과정이었던것 같다.

깃허브 url - https://github.com/hanghae25/facebook_clone_client

0개의 댓글