리액트 심화반 3주차

귀찮Lee·2022년 4월 20일
0

22.04.19(화)
스파르타코딩클럽 리액트 심화반 - 3주차 과정

◎ 포스트 목록 가져오기

  • module 만들기
// src > redux > modules > post.js

// import
import { createAction, handleActions } from "redux-actions"; // Action Creater를 쉽게 만들어줌
import { produce } from "immer"; // 불변성 관련 library

import { firestore } from "../../shared/firebase";
import moment from "moment"

// action
const SET_POST = "SET_POST";
const ADD_POST = "ADD_POST";

// action creators / Using redux-actions
const setPost = createAction(SET_POST, (post_list) => ({post_list}))
const addPost = createAction(ADD_POST, (post) => ({post}))

// initialState
const initialState = {
    list: [],
}

const initialPost = {
    image_url: "https://velog.velcdn.com/images%2Fgwichanlee%2Fpost%2F5cc3bbe0-550a-4cb7-8804-467f420f6002%2Ftest2.jpg",
    contents: "",
    comment_cnt: "0",
    insert_dt: moment().format("YYYY-MM-DD hh:mm:ss"),
};

// reducer Using redux-actions, immer
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);
        }),
    }, initialState
);

// export
const actionCreators={
    setPost,
    addPost,
    getPostFB,
    addPostFB,
}

export {actionCreators}
  • rootreducer에 추가 (Storeconfig 설정)
// redux/configureStore.js
...
import Post from "./modules/post";
...
const rootReducer = combineReducers({
  user: User,
  post: Post,
  router: connectRouter(history),
});
...
  • 게시글 연동하기
// pages/PostList.js
...
import {useSelector} from "react-redux";
import Post from "../components/Post";
...
const PostList = (props) => {
	const post_list = useSelector((state) => state.post.list);
...
return (
        <React.Fragment>
            {post_list.map((p, idx) => {
                return <Post key={p.id} {...p}/>
            })}
        </React.Fragment>
    )
...

◎ 파이어스토어 연동하기

  • Firebase Console에서 Firebase Store 활성화 및 임시 데이터 넣어놓기
  • firebase.js 에 Firestore 추가
 // shared/firebase.js
import "firebase/firestore";
...
const firestore = firebase.firestore();
...
export{auth, apiKey, firestore};
  • firestore에서 데이터 가져오기
//redux/modules/post.js
import { firestore } from "../../shared/firebase";
...
const getPostFB = () => {
  return function (dispatch, getState, { history }) {
    const postDB = firestore.collection("post"); // firestore.collection 가져오기, "post": collection 이름

    postDB.get().then((docs) => {
      let post_list = [];

      // 반복문 사용
      docs.forEach((doc) => { 
        
        // console.log(doc.id, doc.data()); // 데이터 형태 확인

        let _post = doc.data(); // 데이터 내용 가져오기
        // 데이터 모양 수정
        let post = {
            id: doc.id,
            user_info: {
                user_name: _post.user_name,
                user_profile: _post.user_profile,
                user_id: _post.user_id,
            },
            contents: _post.contents,
            image_url: _post.image_url,
            comment_cnt: _post.comment_cnt,
            imsert_dt: _post.insert_dt
        }

        post_list.push(post);
      });

      // 리스트 확인하기!
      console.log(post_list);

      dispatch(setPost(post_list)); // Action을 Dispatch함
    });
  };
};
...
  • useEffect를 통해, getpostFB 실행
// PostList.js
// 최초 로딩시에 실행
React.useEffect(() => {
  dispatch(postActions.getPostFB());
}, []);

◎ 포스트 작성하기

  • 로그인 후에만 /write에 접근하게 하기
    • useSeletor를 이용해 state.user.is_login 값을 가져옴
    • is_login 값이 false 일때, 다른 페이지를 보여줌
    • (+) 버튼 누를 때, history.push("/write")
  • 작성한 게시글 내용 넘겨주기 (contents에 inputbox값 가져오기)
const [contents, setContents] = React.useState('');
...
const changeContents = (e) => {
    setContents(e.target.value);
}
...
<Grid padding="16px">
    <Input _onChange={changeContents} label="게시글 내용" placeholder="게시글 작성" multiLine />
</Grid>
  • firestore에 데이터 넣기
    • moment 설치 (현재 날짜, 시간을 쉽게 사용할 수 있음)
    yarn add moment
    • firestore에 저장하는 함수 만듦 / 성공시, redux에 데이터를 넣음.
    // redux/modules/post.js
    import moment from "moment";
    ...
    const initialPost = {
      image_url: "기본 이미지 url",
      contents: "",
      comment_cnt: 0,
      insert_dt: moment().format("YYYY-MM-DD hh:mm:ss"),
    };
    ...
    const addPostFB = (contents = "") => {
      return function (dispatch, getState, { history }) {
        // DB를 설정
        const postDB = firestore.collection("post");
        // user 정보를 state에서 가져옴
        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);
    		  // DB에 내용 저장	
        postDB.add({...user_info, ..._post}).then((doc) => {
            // 아이디를 추가
            let post = {user_info, ..._post, id: doc.id};
            // redux에 넣어줌
            dispatch(addPost(post));
            // 페이지 이동 / replace: 뒤로가기시, 원래 페이지로 안감
            history.replace("/")
        }).catch((err) => {
            console.log('post 작성 실패!', err);
        });
      };
    };
    ... 
    [ADD_POST]: (state, action) => produce(state, (draft) => {
            // unshift: 배열 맨 앞에 데이터를 넣어줌
             draft.list.unshift(action.payload.post);
        }),

◎ firebase Storage

  • 이미지 업로드시, 이용할 예정

  • Storge 연결하기 및 Firebase Console Rules 세팅

// shared/firebase.js
...
import "firebase/firestore";
import "firebase/storage";
...
const apiKey = firebaseConfig.apiKey;
const auth = firebase.auth();
const firestore = firebase.firestore();
const storage = firebase.storage();

export{auth, apiKey, firestore, storage};
// 파이어베이스 콘솔 -> Storage에서 규칙(rules) 탭으로 이동!아래처럼 바꿔주기!
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
			allow read, write: if request.auth != null;
    }
  }
}
  • Storage 업로드
// Upload.js
import React from "react";
import { Button } from "../elements";
import {storage} from "./firebase";

const Upload = (props) => {
  const fileInput = React.useRef();

  const selectFile = (e) => {
    // e.target은 input이죠!
    // input이 가진 files 객체를 살펴봅시다.
    console.log(e.target.files);
    // 선택한 파일이 어떻게 저장되어 있나 봅시다.
    console.log(e.target.files[0]);
    // ref로도 확인해보자
    console.log(fileInput.current.files[0]);
  };

  const uploadFB = () => {
      let image = fileInput.current?.files[0];
      const _upload = storage.ref(`images/${image.name}`).put(image);

       //업로드
    _upload.then((snapshot) => {
      console.log(snapshot);
      // 업로드한 파일의 다운로드 경로를 가져옴
      snapshot.ref.getDownloadURL().then((url) => {
        console.log(url);
      });
    });

  }

  return (
    <React.Fragment>
      <input type="file" ref={fileInput} onChange={selectFile} />
      <Button _onClick={uploadFB}>업로드하기</Button>
    </React.Fragment>
  );
};

export default Upload;
  • 이미지 모듈 만들기
    • Action : 이미지를 파이어베이스에 올리고, 이미지 링크를 redux에 저장 / 이미지가 업로드 중 일때, 파일 선택을 막음( true값을 지님 )
    • configureStore.js 에 추가
    • Upload.js에 연결

◎ Preview 만들기

  • 문제점 : 실제 올릴 사진이 아닌 다른 사진을 업로드해도, 계속 업로그 된다.
    -> 미리 업로드되는 사진을 보여주고, 마지막 게시글 올릴 떄만, 사진이 업로드 되게 한다.
  • FileReader (파일 선택시, 읽어보기)
const selectFile = (e) => {
  const reader = new FileReader();
  const file = e.target.files[0]; // onchange로 값을 가져옴

  // 파일 내용을 읽어옵니다.
  reader.readAsDataURL(file);

  // 읽기가 끝나면 발생하는 이벤트 핸들러예요! :)
  reader.onloadend = () => {
    // reader.result는 파일의 컨텐츠(내용물)입니다!
    console.log(reader.result);
  };
};
  • Action 만들기 (upload.js)
// actions
const SET_PREVIEW = "SET_PREVIEW";

// action creators
const setPreview = createAction(SET_PREVIEW, (preview) => ({ preview }));

const initialState = {
  ...
  preview: null,
};

//reducer
...
[SET_PREVIEW]: (state, action) =>
      produce(state, (draft) => {
        draft.preview = action.payload.preview;
      }),
// 이후 export처리
  • 미리보기 이미지에 넣어주기
const preview = useSelector((state) => state.image.preview);

<Image shape="rectangle" src={preview ? preview : "http://via.placeholder.com/400x300"}/>
  • onChange 시에, Action을 dispatch함
const selectFile = (e) => {
  const reader = new FileReader();
  const file = e.target.files[0]; // onchange로 값을 가져옴

  // 파일 내용을 읽어옵니다.
  reader.readAsDataURL(file);

  // 읽기가 끝나면 발생하는 이벤트 핸들러예요! :)
  reader.onloadend = () => {
    // reader.result는 파일의 컨텐츠(내용물)입니다!
    console.log(reader.result);
    dispatch(imageActions.setPreview(reader.result)); // Action을 dispatch함
  };
};

◎ 게시글 작성할 때 이미지를 업로드

  • state에서 데이터 가져오기
const _image = getState().image.preview;
console.log(typeof _image); // String
  • string 형태의 파일 업로드
// 파일 이름은 유저의 id와 현재 시간을 밀리초로 넣어줍시다! (혹시라도 중복이 생기지 않도록요!)
const _upload = storage
      .ref(`images/${user_info.user_id}_${new Date().getTime()}`)
      .putString(_image, "data_url");
  • addPostFB 미들웨어 수정
    • preview image 데이터 가져옴
    • storage에 이미지 업로드 / 이미지 주소 가져옴
    • Database에 이미지 주소를 포함한 Post 데이터를 Database에 저장

◎ Debounce와 Throttle

  • onChange같은 경우, 쓸모없는 event가 많이 일어난다면, 쓸모없는 API 요청과 수많은 랜더링을 일으킬 수 있다.

  • debounce : 이벤트 발생시, 일정 시간 기다렸다가 이벤트를 수행. 기다리는 중에 같은 이벤트가 들어오면, 다시 일정 시간을 기다렸다가 수행
    / 반복되는 이벤트가 중단되어야 실행함

  • throttle : 일정 시간동안 일어난 이벤트를 모아서 같은 이벤트는 한번만 실행함
    / 계속 실행되는 이벤트에서 일정 시간 간격으로 이벤트가 실행한다.

◎lodash로 이벤트 관리하기

  • lodash : 자바스크립트 유틸리티 라이브러리, 배열 관리부터 모듈화, 성능 향상과 관련된 것까지 엄청 많은 기능을 제공, debounce와 throttle도 제공

  • useCallback: 함수를 메모지에이션 함 (다른 곳에 저장해놓음)
    -> 리랜더링 시에도 정상적으로 진행함

// shared/Search.js
import React from "react";
import _ from "lodash"; // lodash 부르기

const Search = () => {
  const [text, setText] = React.useState("");
  const debounce = _.debounce((k) => console.log("디바운스! :::", k.target.value), 1000);
  const throttle = _.throttle((k) => console.log("스로틀! :::", k.target.value), 1000);

  // useCallback : 함수를 메모지에이션 함 (다른 곳에 저장해놓음) // 인자: 실행시킬 함수, Array: value 변경시 초기화시킬 인자
  // 최적화시에 매우 중요
  const keyPress = React.useCallback(debounce, []);
  
  const onChange = (e) => {
     setText(e.target.value); // Search 함수형 컴포넌트 : text가 바뀔때마다 리랜더링이 일어남
     debounce(e); // text가 바뀔때마다 리랜더링이 일어남(state가 바뀜) -> debounce가 초기화됨
  };

  const onChange2 = (e) => {
     setText(e.target.value); // Search 함수형 컴포넌트 : text가 바뀔때마다 리랜더링이 일어남
     keyPress(e) // 메모지에이션이 되어 리랜더링 되어도, 정상적으로 실행
  };

  return (
    <div>
      <h3>Debounce</h3>
      <input type="text" onChange={debounce} />
      <h3>Throttle</h3>
      <input type="text" onChange={throttle} />
      <h3>Debounce, useState를 같이 사용</h3>
      <h3>Search 함수형 컴포넌트 : text가 바뀔때마다 리랜더링이 일어남(state가 바뀜) -> debounce가 초기화됨</h3>
      <input type="text" onChange={onChange} />
      <h3>useCallback 사용</h3>
      <input type="text" onChange={onChange2}/>
      <h2>콘솔창 확인</h2>
    </div>
  );
};

export default Search;
profile
장비를 정지합니다.

0개의 댓글

관련 채용 정보