본문 바로가기
React

리액트 심화 3주차 정리

by imagineer_jinny 2021. 3. 30.

[수업 목표]

  1. 파이어스토어로 데이터를 관리한다.
  2. 파이어베이스의 스토리지 서비스를 사용하여 이미지 업로드 기능을 만든다.
  3. 파일 업로드 전 이미지 미리보기를 하려면 어떻게 하는 지 알아본다.
  4. 잦은 이벤트 처리 기법에 대해 알아본다.

Debounce와 Throttle

이벤트가 엄청 많이 일어나면?

 

우리가 감사합니다를 검색 할 때, ㄱ, ㅏ, ㅁ, ㅅ, ㅏ 하나하나 입력할 때마다 검색을 새로 하면(=검색 api를 호출한다고 해봅시다!), 연관 검색어 같은 걸 빨리 바꿔줄 수 있어서 좋죠. 그런데 감사합니다를 빨리 검색한다고 생각해보세요. 1초도 안되는 시간에 이미 감사합까지 쳤다면, ㄱ,ㅏ,ㅁ,..., ㅎ,ㅏ,ㅂ까지 엄청나게 많이 검색을 해야해요. 😢

이럴 땐 특정 시간마다 1번씩, 혹은 키보드 입력을 멈췄을 때만 1번 해주는 게 검색 횟수를 줄일 수 있어요!

특히 api를 불러야 할 때는, 순식간에 엄청 많은 서버 요청이 날아가는 걸 막아줄 수 있어요!

→ 물론 연관검색어 부분의 엄청난 랜더링도 막아줄 수 있겠죠!

 

이벤트를 관리할 수 있는 두 가지 방법

1) debounce

이벤트가 일어나면, 일정 시간을 기다렸다가 이벤트를 수행해요! 일정 시간 내에 같은 이벤트가 또 들어오면 이전 요청은 취소해요.

새로운 이벤트가 들어오면 거기에서부터 1초를(일정 시간을 1초라고 설정) 다시 기다리는게 debouce!

2) throttle

일정 시간 동안 일어난 이벤트를 모아서 주기적으로 1번씩 실행해줘요.

1초 동안 일어났던 이벤트들을 캔슬하는게 아니라 갖고 있고 마지막 것만 한번 실행 해줌

일정 시간 동안 일어난 이벤트를 모아가지고 가장 최근 것을 실행해줌

 

둘중 뭘 써? 상황 따라 다름!

 

 

lodash로 이벤트 관리하기!

-useCallback():

함수를 어디에다 저장을 해 놓음. 

그래서 이 컴포넌트가 리렌더링이 되더라도 함수를 초기화하지 마라!

내가 메모이제이션한 저장한 친구 계속 쓸거야!라고 하는 것!

const keyPress = React.useCallback(throttle, []);

throttle: 앞의 인자에는 함수를 넣음. 어떤걸 구동시킬건지.

[ ]: 그 뒤에다가는 array로 무언가가 예를들어 [text]면 text가 변할 때

이 함수도 변할거야. 라는식으로 주는것임.

이 함수를 초기화할 조건을 주는것임

 

 

<3주차 과제>

1.알림페이지 만들기

1) 일단 pages/Notification.js 만들고 기본 설정 해준 다음에 기획서 보고 넣어야 할 것들을 생각함.

2) 기본적으로 넣어야하는 데이터(이미지, 유저인포, 유저네임)를 가짜 데이터 만들어서 넣어줌. 

3) 또 칸 하나를 누르면 그 게시물의 상세 페이지로 가야한다는 것 확인. 그렇다면 포스트 아이디도 가지고 와야한다는 뜻!

4)이 페이지를 일단 가짜데이터 넣어서 기본적인걸 채워주면

import React from "react";
import {Grid, Text, Image} from "../elements";

const Notification =(props)=>{
    //기본적으로 넣어야하는 데이터를 가짜데이터 만들어서 넣어줌
    //기획서 보니까 이미지, 유저인포, 유저네임
    //또 칸 하나 누르면 어디로 가야하냐면 그 게시물의 상세 페이지로 gogo
    //그럼 포스트 아이디도 가지고 와야 함
    let noti=[
        {user_name:"aaaa", post_id:"post1",image_url:""},
        {user_name:"aaaa", post_id:"post2",image_url:""},
        {user_name:"aaaa", post_id:"post3",image_url:""},
        {user_name:"aaaa", post_id:"post4",image_url:""},
        {user_name:"aaaa", post_id:"post5",image_url:""},   
    ];

    return(
    <React.Fragment>
        {/* 임시 데이터 만들어줬으면 뷰 만들어줘야함 */}
        <Grid padding="16px" bg="#EFF6FF">
            {/* 이 안에 들어가야 할것들은 카드! */}
            {noti.map((n)=>{
               return(
                //    여기는 하나하나 흰 카드 부분, is_flex는 가로 정렬
                <Grid padding="16px" is_flex bg="#ffffff" margin="8px 0px">
                  {/* 여기는 이미지 부분, 일단 디폴트로 넣어줘 */}
                  <Grid width="auto" margin="0px 8px 0px 0px">
                      <Image/> 
                  </Grid>
                  {/* 여기는 텍스트 부분 */}
                  <Grid>
                      <Text>
                          {/* 특정 글자만 볼드 해줄 때 b 태그  */}
                         <b>{n.user_name}</b>님이 게시글에 댓글을 남겼습니다:)!
                      </Text>
                  </Grid>

                </Grid>
               ) 
            })}
        </Grid>

    </React.Fragment>
        
    )
}

export default Notification;

5)이렇게 하고 어떻게 한다?

이제 노티피케이션 페이지에 우리가 만든 이 노티피케이션 컴포넌트를 불러와야 함!

근데 불러오려고 하고 봤더니 지금 연결이 안되어있음

연결해줘야함! 어디로 가? 

 

6)App.js로 들어와서 라우트 설정해줘! 

라우트 설정하기 전에 App.js에서 import부터 해주는거 잊지말기

import Notification from "../pages/Notification";
<Route path="/noti" exact component={Notification}/>

7) 그런데 이미지 사진을 동그라미가 아닌 정사각형으로 주고싶다!

그럼 어떻게 해? 이미지 태그 들어가고 또 Image.js 들어가서 뭘 해줘야겠네?

 

8)Image.js 들어왔어

 return (
        <React.Fragment>
            {/* 기본적으로 정사각형 네모가 나오도록 여기 리턴에다가 이미지 디폴트 하나 만들기 */}
            <ImageDefault {...styles} ></ImageDefault>
        </React.Fragment>
    )

근데 {...styles}는 뭘 받아오는거야? 위에 얘를 받아오는거야

const Image = (props) => {
    const {shape, src, size} = props;

    const styles = {
        src: src,
        size: size,
    }

 

그리고 ImageDefault라는 함수를 하나 더 만들어줬어

const ImageDefault= styled.div`
    --size: ${(props) => props.size}px;
    width: var(--size);
    height: var(--size);
    background-image: url("${(props) => props.src}");
    background-size: cover;
    
`;

9)그리고 다시 노티피케이션 들어와서 Image 설정해주기

10) 화면모습

11) 근데 얘네를 중간 컴포넌트로 묶어주고 싶어!

그래서 components 폴더 아래에 Card.js 컴포넌트 하나 만들어서 묶어주겠음.

왜 묶어주고 싶냐면 저 뷰 자체가 카드 하나하나들이 모인건데 그럼 이 하나하나를 컴포넌트로 빼면

얘 하나를 그냥 갖다 쓰면 되니깐 

12) 카드 만들어서 notification에 다시 넣어주기

 return (
      <React.Fragment>
          {/* 임시 데이터 만들어줬으면 뷰 만들어줘야함 */}
        <Grid padding="16px" bg="#EFF6FF">
            {/* 이 안에 들어가야 할것들은 카드! */}
          {noti.map((n) => {
            return <Card {...n} key={n.post_id} />;
          })}
        </Grid>
      </React.Fragment>
    );

13) 이제 알림 버튼 눌렀을 때 noti 페이지로 이동해주는 것 도전

그거 해주려면 어디로 가야한다? Header.js로 간다!

Header.js에서 알림쪽으로 간다.

알림쪽 온클릭에 히스토리 푸시 노티..

<Grid is_flex>
    <Button text="내정보"></Button>
    <Button text="알림" _onClick={() => {
       history.push('/noti');
     }}></Button>
     <Button text="로그아웃" _onClick={() => {
         dispatch(userActions.logoutFB());
     }}></Button>
</Grid>

 

2.게시글 수정하기

1)게시글 수정 하려면 수정 버튼이 있어야 하고, 또 내가 쓴 게시글일 경우에만 수정버튼이 보여야 함!

기획서에서 수정버튼 잡아줄 위치 확인하고, 게시글을 확인.

게시글 하나에는 어떤 정보가 담겨있어? 이 하나의 유저 정보가 들어있고, 나도 로그인을 했으니까 유저 정보를 가지고 있음. 어디에 가지고 있지? 유저 리덕스!

redux state 보면 스토어 안에 이런것들이 있는데 uid를 가지고 비교해주겠음!

2) component 안에 있는 Post.js에 가주기!

근데 Post에서 비교를 해주는 것 보다는 PostList에서 유저정보를 가지고 와서 비교를 해주는게 더 나음!

리스트를 갖고있는 애가 const PostList니까!

//PostList.js
const PostList =(props)=>{
    const dispatch=useDispatch();
    const post_list = useSelector((state) => state.post.list);
    
    //유저 인포 가지고 와주기
    const user_info = useSelector((state) => state.user.user);
    console.log(post_list);
    console.log(user_info);

3)유저 인포 가지고 왔으면 이제 포스트 하나하나에다가 이게 내가 맞아 아니야 라는 값을

is_me라는 값으로 넘겨줘볼거야! 어떻게 넘겨줘? if 해가지고 

p에는 뭐가 들어있다고? user_info 라는게 들어있다고. 이 안에 user_id라는게 있겠죠?

이게 뭐랑 똑같으면? 내가 가지고 온 user_info의 uid랑 똑같으면!

그리고 그게 아니라고 하면 그냥 리턴 해버리면 될 것.

그 안에 is_me를 주면 되겠다.

//PostList.js
    return(
        <React.Fragment>
            {post_list.map((p, idx) => {

                console.log(p);
                
                if(user_info && p.user_info.user_id === user_info.uid){
                    return <Post key={p.id} {...p} is_me/>
                }
                return <Post key={p.id} {...p} />
            })}
        </React.Fragment>
    )

}

하나 더 신경써야할 경우가 있는데 user_info가 null일 경우가 있음 언제? 로그인을 안 했을때!

로그인을 안했을 때는 user_info 아래에 있는 값을 찾을 수 없음! 이럴때 user_info가 있는지 먼저 체크해주거나 옵셔널 체이닝 써주기

 

4) 그 다음 Post 컴포넌트로 넘어가주기!

포스트 컴포넌트에서 is_me일 경우에 수정 버튼이 보이게 해줄 것.

어디에다? insert_dt 앞에다가!

//Post.js
import {Grid, Image, Text, Button} from "../elements";
import { history } from "../redux/configureStore";

//...
<Grid is_flex width="auto">
    {props.is_me && (<Button width="auto" padding="4px" margin="4px" _onClick={() => {history.push(`/write/${props.id}`)}}>수정</Button>)}
    <Text>{props.insert_dt}</Text>
</Grid>

//...

  contents: "블라디보스톡에서..",
  comment_cnt: 10,
  insert_dt: "2021-02-27 10:00:00",
  is_me: false,
};

5) 수정 버튼 안이뻐서 Button.js 가서 버튼 속성 좀 만져주고

6)이건 위에서 수정 버튼 누르면 페이지로 넘어가는 코드

{history.push(`/write/${props.id}`)}}>

7) 다시 App.js 들어가서 수정 페이지로 잘 넘어가게 Route 추가해주기 

<Route path="/write/:id" exact component={PostWrite}/>

8)이제 다시 PostWrite 폴더로 가자

콘솔로 이렇게 치면 콘솔에 아이디가 들어올 것임. 이 콘솔에 아이디 들어온것을 가지고

 얘가 수정이다 아니다를 판별할 수 있음.

왜냐하면 그냥 게시글 작성에서는 /write 뒤에 아무것도 없으니까.

보면 undefined로 나옴

console.log(props.match.params.id);

9)이걸가지고 수정중이다 아니다를 판별하겠음.

PostWrite, Input 손봐주기 : 이미지랑 텍스트 등 수정하게 해줌

import React from "react";
import {Grid, Text, Button, Image, Input} from "../elements";
import Upload from "../shared/Upload";
import {useSelector, useDispatch} from "react-redux";
import {actionCreators as postActions} from "../redux/modules/post";
import { actionCreators as imageActions } from "../redux/modules/image";

const PostWrite = (props) => {
  const dispatch = useDispatch();
  const is_login = useSelector((state) => state.user.is_login);
  const preview = useSelector((state) => state.image.preview);
  const post_list = useSelector((state) => state.post.list);

  console.log(props.match.params.id);

  //변수들 만들기
  const post_id = props.match.params.id; //주소창에서 params로 넘어온 id를 갖고 있는 애
  const is_edit = post_id ? true : false; //post_id가 있다면 true를 주고 없으면 false를 반환
 
  const {history} = props;
  
  //포스트 데이터 가져오기
  let _post = is_edit ? post_list.find((p) => p.id === post_id) : null;  
  
  console.log(_post);

  const [contents, setContents] = React.useState(_post ? _post.contents : "");

  const changeContents = (e) => {
    setContents(e.target.value);
    
  }

  //포스트 정보가 없으면 뒤로가기 해주기
  React.useEffect(() => {
    if (is_edit && !_post) {
      console.log("포스트 정보가 없어요!");
      history.goBack();

      return;
    }

    if (is_edit) { 
      //수정모드일때만 이미지 가져오겠다
      dispatch(imageActions.setPreview(_post.image_url));
    }
  }, []);


  const addPost = () => {
    dispatch(postActions.addPostFB(contents));
  }
  
  if(!is_login){
    return (
      <Grid margin="100px 0px" padding="16px" center>
        <Text size="32px" bold>
          앗! 잠깐!
        </Text>
        <Text size="16px">
          로그인 후에만 글을 쓸 수 있어요!
        </Text>
        <Button
          _onClick={() => {
            history.replace("/login");
          }}
        >
          로그인 하러가기
        </Button>
      </Grid>
    );
  }
    return (
      <React.Fragment>
        <Grid padding="16px">
          <Text margin="0px" size="36px" bold>
            게시글 작성
          </Text>
          <Upload/>
        </Grid>

        <Grid>
          <Grid padding="16px">
            <Text margin="0px" size="24px" bold>
              미리보기
            </Text>
          </Grid>

          <Image
            shape="rectangle"
            src={preview ? preview : "http://via.placeholder.com/400x300"}
          />
          
        </Grid>

        <Grid padding="16px">
          <Input 
          value={contents}
          _onChange={changeContents} 
          label="게시글 내용"
          placeholder="게시글 작성" 
          multiLine />
        </Grid>

        <Grid padding="16px">
          <Button text="게시글 작성" _onClick={addPost}></Button>
        </Grid>
      </React.Fragment>
    );
}

export default PostWrite;

10) 이제 게시글 작성 누르면 ADD 말고 수정 되게 해주기

어떻게 해 ? 이 게시글 작성 자체를 is_edit인지 아닌지 확인해서 보여주면 되는 일!

버튼도 바꿔주자!

editPost라는 함수를 하나 만들어주기!

여기에서 dispatch해가지고 뭔가 액션을 실행하려고 하면 정말로 수정하는 액션이 있어야 함.

수정하는 액션 어디서 만들어줌? 리덕스 modules 아래 post.js에서 만들어줌!

 const editPost = () => {
    dispatch(postActions.editPostFB(post_id, {contents: contents}));
  }

 

11) post.js로 가자!

12) 그리고 실제로 파이어 스토어 상에서도 수정이 되야 하기 때문에 작업이 더 필요

post.js 코드!

import { createAction, handleActions } from "redux-actions";
import { produce } from "immer";
import {firestore, storage} from "../../shared/firebase";
import moment from "moment";

import {actionCreators as imageActions} from "./image";

const SET_POST = "SET_POST";
const ADD_POST = "ADD_POST";
const EDIT_POST = "EDIT_POST";

const setPost = createAction(SET_POST, (post_list) => ({post_list}));
const addPost = createAction(ADD_POST, (post) => ({post}));
//수정을 하려면 어떤 정보가 있어야 수정을 할 수 있을까?
//일단 post 아이디가 있어야 하고, 수정할 내용물들 즉 포스트 딕셔너리 필요함
const editPost = createAction(EDIT_POST, (post_id, post) => ({
  post_id,
  post,
}));

const initialState = {
    list: [],
}

// 게시글 하나에는 어떤 정보가 있어야 하는 지 하나 만들어둡시다! :)
const initialPost = {
  user_info: {
		id: 0,
    user_name:  "jinnypony",
    user_profile: "https://jieunpic.s3.ap-northeast-2.amazonaws.com/KakaoTalk_20210326_155907258.jpg",
  },
    image_url: "https://jieunpic.s3.ap-northeast-2.amazonaws.com/KakaoTalk_20210326_155907258.jpg",
    contents: "",
    comment_cnt: 0,
    insert_dt: moment().format("YYYY-MM-DD hh:mm:ss"),
};
//파이어스토어 상에서도 수정 되어야하기때문에
//똑같이 포스트 아이디 가져와야 하고 
//포스트 정보 갖다가 수정해야 함

const editPostFB = (post_id = null, post = {}) => {
  return function (dispatch, getState, { history }) {
    //이미지는 수정이 될 수도 있고 안될수도 있음
    //이미지 수정한다는건 이미지를 새로 올린다는 것임.
    //그럼 이미지를 업로드 했다 안했다를 어떻게 체크하면 좋을까?
    //이미지를 새로 업로드 한 경우에 input에 파일이 들어가 있다가 그 파일을
    //파일 리더로 읽어서 preview에 넣어줬음(data url로 들어가있음 프리뷰에)
    //그런데 만약 파일을 안올렸으면 우리가 임의로 넣어줬던 포스트 하나에 들어가있던
    //이미지 링크가 들어가 있을 것임!
    //그러면 우리는 하나의 링크랑 포스트에 이미 있던 링크랑
    //프리뷰에 저장되어있는 값이랑 비교를 해가지고 똑같으면 새로 업로드 안한거고
    //다르면 뭔가 업로드를 한것임. 프리뷰에다가!
   
    // 포스트 아이디가 없으면 재빨리 리턴부터 해주겠다.
    if (!post_id) {
      console.log("게시물 정보가 없어요!");
      return;
    }

    const _image = getState().image.preview;

    const _post_idx = getState().post.list.findIndex((p) => p.id === post_id);
    const _post = getState().post.list[_post_idx];

    console.log(_post);

    const postDB = firestore.collection("post");
    
    //같으면 이미지 새로 업로드 안해준거임
    //그럼 게시글만 가져오면 됨
    if (_image === _post.image_url) {
      postDB
        .doc(post_id)
        .update(post)
        .then((doc) => {
          dispatch(editPost(post_id, { ...post }));
          history.replace("/");
        });

      return;
    } else { 
      //이미지도 바꿔주기
      const user_id = getState().user.user.uid;
      const _upload = storage
        .ref(`images/${user_id}_${new Date().getTime()}`)
        .putString(_image, "data_url");

      _upload.then((snapshot) => {
        snapshot.ref
          .getDownloadURL()
          .then((url) => {
            console.log(url);

            return url;
          })
          .then((url) => {
            postDB
              .doc(post_id)
              .update({ ...post, image_url: url })
              .then((doc) => {
                dispatch(editPost(post_id, { ...post, image_url: url }));
                history.replace("/");
              });
          })
          .catch((err) => {
            window.alert("앗! 이미지 업로드에 문제가 있어요!");
            console.log("앗! 이미지 업로드에 문제가 있어요!", err);
          });
      });
    }
  };
};

const addPostFB = (contents = "") => {
  return function (dispatch, getState, { history }) {
    const postDB = firestore.collection("post");

    const _user = getState().user.user;
    const user_info = {
      user_name: _user.user_name,
      user_id: _user.uid,
      user_profile: _user.user_profile,
    };

    const _post = {
      ...initialPost,
      contents: contents,
      insert_dt: moment().format("YYYY-MM-DD hh:mm:ss")
    };

    // 잘 만들어졌나 확인해보세요!!
    console.log(_post);

    // getState()로 store의 상태값에 접근할 수 있어요!
    const _image = getState().image.preview;

    // post.js에서 확인해봐요!
    console.log(typeof _image);

    // 파일 이름은 유저의 id와 현재 시간을 밀리초로 넣어줍시다! (혹시라도 중복이 생기지 않도록요!)
    const _upload = storage
      .ref(`images/${user_info.user_id}_${new Date().getTime()}`)
      .putString(_image, "data_url");

    _upload
      .then((snapshot) => {
        snapshot.ref
          .getDownloadURL()
          .then((url) => {
            // url을 확인해봐요!
            console.log(url);
            dispatch(imageActions.uploadImage(url));
            return url;
          })
          .then((url) => {
            // return으로 넘겨준 값이 잘 넘어왔나요? :)
            // 다시 콘솔로 확인해주기!
            console.log(url);

            postDB
              .add({ ...user_info, ..._post, image_url: url })
              .then((doc) => {
                // 아이디를 추가해요!
                let post = { user_info, ..._post, id: doc.id, image_url: url };
                // 이제 리덕스에 넣어봅시다.
                dispatch(addPost(post));
                history.replace("/");

                dispatch(imageActions.setPreview(null));
              })
              .catch((err) => {
								window.alert("앗! 포스트 작성에 문제가 있어요!");
                console.log("post 작성 실패!", err);
              });
          });
      })
      .catch((err) => {
				window.alert("앗! 이미지 업로드에 문제가 있어요!");
        console.log(err);
      });
  };
};

const getPostFB = () => {
  return function (dispatch, getState, { history }) {
    const postDB = firestore.collection("post");

    postDB.get().then((docs) => {
      let post_list = [];
      docs.forEach((doc) => {
        let _post = doc.data();

        // ['commenct_cnt', 'contents', ..]
        let post = Object.keys(_post).reduce(
          (acc, cur) => {
            if (cur.indexOf("user_") !== -1) {
              return {
                ...acc,
                user_info: { ...acc.user_info, [cur]: _post[cur] },
              };
            }
            return { ...acc, [cur]: _post[cur] };
          },
          { id: doc.id, user_info: {} }
        );

        post_list.push(post);
      });

      console.log(post_list);

      dispatch(setPost(post_list));
    });
  };
};

export default handleActions(
  {
    [SET_POST]: (state, action) =>
      produce(state, (draft) => {
        draft.list = action.payload.post_list;
      }),

    [ADD_POST]: (state, action) =>
      produce(state, (draft) => {
        draft.list.unshift(action.payload.post);
      }),
    [EDIT_POST]: (state, action) =>
      produce(state, (draft) => {
        let idx = draft.list.findIndex((p) => p.id === action.payload.post_id);

        draft.list[idx] = { ...draft.list[idx], ...action.payload.post };
      }),
  },
  initialState
);

const actionCreators = {
  setPost,
  addPost,
  editPost,
  getPostFB,
  addPostFB,
  editPostFB,
};

export { actionCreators };


 

'React' 카테고리의 다른 글

클론코딩 첫 주 - 프로그래머스 사이트  (0) 2021.04.05
파이어베이스 배포 안될 때  (0) 2021.04.02
리액트 심화 4주차 정리  (0) 2021.04.01
리액트 심화 2주차 정리  (0) 2021.03.29
리액트 심화 1주차 정리  (0) 2021.03.27

댓글