Advanced Course 주특기 2강 - React

심화 주특기 2강!

  1. 토큰에 대해 알아본다.
  2. 파이어베이스를 사용해서 로그인 기능을 구현해본다.
  3. 로그인을 유지하는 방법에 대해 알아본다.
  4. 로그인 권한을 체크하는 컴포넌트를 만들어본다.

Promise

// 프라미스 객체를 만듭니다. 
// 인자로는 (resolve, reject) => {} 이런 excutor 실행자(혹은 실행 함수라고 불러요.)를 받아요.
// 이 실행자는 비동기 작업이 끝나면 바로 두 가지 콜백 중 하나를 실행합니다.
// resolve: 작업이 성공한 경우 호출할 콜백
// reject: 작업이 실패한 경우 호출할 콜백
const promise = new Promise((resolve, reject) => {
  if(...){
     ...
     resolve("성공!");
  }else{
    ...
    reject("실패!");
  }
});
  • promise의 상태값
    1. pending: 비동기 처리 수행 전(resolve, reject가 아직 호출되지 않음)
    2. fulfilled: 수행 성공(resolve가 호출된 상태)
    3. rejected: 수행 실패(reject가 호출된 상태)
    4. settled: 성공 or 실패(resolve나 reject가 호출된 상태)
  • promise 후속 처리 메서드
    1. then(성공 시, 실패 시)
    then의 첫 인자는 성공 시 실행, 두번째 인자는 실패 시 실행됩니다. (첫 번째 인자만 넘겨도 됩니다!)
let promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("완료!"), 1000);
});

// resolve
promise.then(result => {
  console.log(result); // 완료!가 콘솔에 찍힐거예요.
}, error => {
  console.log(error); // 실행되지 않습니다.
}).finally(() => {
  console.log('finally'); // promise 마지막에 무조건 실행.
});
let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("오류!")), 1000);
});

// reject
promise.then(result => {
  console.log(result); // 실행되지 않습니다.
}, error => {
  console.log(error); // Error: 오류!가 찍힐거예요.
});

then을 연달아서 이어주는 방법 (promise chaining)

new Promise((resolve, reject) => {
  setTimeout(() => resolve("promise 1"), 1000);
}).then((result) => { // 후속 처리 메서드 하나를 쓰고,
  console.log(result); // promise 1
  return "promise 2";
}).then((result) => { // 위 then의 return 값이었던 promise 2가 result로 들어감
  console.log(result);
  return "promise 3";
}).then(...);
  1. .catch(실패 시)
let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("오류!"), 1000);
});

promise.catch((error) => {console.log(error};);

async & await & generator

async

함수 앞에 async를 붙여서 사용한다. promise로 감싸서 반환한다.

async function fetchUser() {
  return '성공'; // promise와 달리 별다른 선언 없이도 간편하게 비동기 구문을 쓸 수 있음.
}

const user = fetchUser();
user.then(console.log);
// then(result => console.log(result))
// 위와 같이 result를 생략할 수 있음.
console.log(user);

await

async 없이는 사용 못한다. async 함수 안에서만 동작한다.
await은 promise가 처리될 때가지 기다렸다가 그 이후에 결과를 반환한다.

// 첫번째 예제
async function myFunc(){
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000);
  });

  console.log(promise); // pending 상태

  let result = await promise; // 여기서 기다리자!하고 신호를 줍니다.

  console.log(promise);

  console.log(result); // then(후처리 함수)를 쓰지 않았는데도, 1초 후에 완료!가 콘솔에 찍힐거예요.
}
// 두번째 예제
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function getApple() {
  await delay(2000); // 2초간 여기서 기다림
  return '🍎';
}

async function getBanana() {
  await delay(1000);
  return '🍌';
}

async function pickFruits() {
  // 아래처럼 get 함수를 먼저 호출하면 return 값은 미리 할당해둘 수 있음.
  // 즉, 병렬적으로 처리를 할 수가 있는데, 이는 총 2초(apple)가 걸림.
  // 이렇게 하지 않으면? -> 아래 주석 참고
  const applePromise = getApple();
  const bananaPromise = getBanana();
  const apple = await applePromise;
  const banana = await bananaPromise;
  return `${apple} + ${banana}`;
}

// 아래처럼 get 함수를 먼저 호출해서 할당해두지 않으면, apple에 2초 걸린 후 banana에 1초 걸림. (await 때문에 기다려야하므로...)
// async function pickFruits() {
  // const apple = await getApple();
  // const banana = await getBanana();
  // return `${apple} + ${banana}`;
// }

pickFruits().then(console.log);

병렬적으로 처리하는 비동기 구문은 위처럼(pickFruits() 함수처럼) 쓰지 않고 더 예쁘게 쓸 수 있다...!!!
Promise.all 메서드를 이용!

function pickAllFruits() {
  return Promise.all([getApple(), getBanana()]).then(fruits =>
    fruits.join(' + ')
  );
}
pickAllFruits().then(console.log);

가장 빨리 전달된 값 하나만 뽑고싶다면??
Promise.race 메서드를 이용!

function pickOnlyOne() {
  return Promise.race([getApple(), getBanana()]);
}
pickOnlyOne().then(console.log);

generator

이터러블(이터레이션을 돌릴수 있는)을 생성하는 함수이다. 배열도 이터러블 객체이고, 문자열도 이터러블 객체이다.
제너레이터 함수는 비동기 처리에 유용하게 사용된다.

function* counter() { // 제너레이터 함수의 특이한 정의 형태. *을 붙여줌.
  console.log('첫번째 호출');
  yield 1;                  // 첫번째 호출 시에 이 지점까지 실행된다.
  console.log('두번째 호출');
  yield 2;                  // 두번째 호출 시에 이 지점까지 실행된다.
  console.log('세번째 호출');  // 세번째 호출 시에 이 지점까지 실행된다.
  return 3;
}

const generatorObj = counter();

console.log(generatorObj.next()); // 첫번째 호출 {value: 1, done: false}
console.log(generatorObj.next()); // 두번째 호출 {value: 2, done: false}
console.log(generatorObj.next()); // 세번째 호출 {value: 3, done: true}

for(let value of generatorObj) {
  alert(value); // 1, 2가 출력됨. return 값은 무시함.
}

제너레이터 함수는 생성을 해두고 호출하면 객체를 반환하는 특이한 함수이다.
그 객체를 이용해 사용하고 싶을때 사용한다면 비동기 처리하듯 이용할 수 있을것이다.

yarn 설치 패키지들...

redux

yarn add redux react-redux redux-thunk redux-logger [email protected] [email protected]

immer

yarn add immer redux-actions

firebase

yarn add firebase 

기록해두면 재활용하기 좋은 파일들...

image-community 앱 참고!

configureStore.js

// configureStore.js
import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import { createBrowserHistory } from "history";
import { connectRouter } from "connected-react-router";

import User from "./modules/user";

export const history = createBrowserHistory();

const rootReducer = combineReducers({
  user: User,
  router: connectRouter(history),
});

const middlewares = [thunk.withExtraArgument({ history: history })];

// 지금이 어느 환경인 지 알려줘요. (개발환경, 프로덕션(배포)환경 ...)
const env = process.env.NODE_ENV;

// 개발환경에서는 로거라는 걸 하나만 더 써볼게요.
if (env === "development") {
  const { logger } = require("redux-logger"); // 배포환경에서는 import 안되도록 개발환경에서만 require로 불러옴.
  middlewares.push(logger);
}

const composeEnhancers =
  typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
    ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
        // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
      })
    : compose;

const enhancer = composeEnhancers(applyMiddleware(...middlewares));

let store = (initialStore) => createStore(rootReducer, enhancer);

export default store();

firebase.js

//firebase.js
import firebase from "firebase/app";
import "firebase/auth";

const firebaseConfig = {
  apiKey: "AIzaSyB7jyeMM1j2DcPl02FK8_mIq-Hn8MXL9Oo",
  authDomain: "sparta-react-f05ca.firebaseapp.com",
  projectId: "sparta-react-f05ca",
  storageBucket: "sparta-react-f05ca.appspot.com",
  messagingSenderId: "387824718666",
  appId: "1:387824718666:web:1588254195b4dce37d27b3",
  measurementId: "G-YJF8QHWTWF",
};

firebase.initializeApp(firebaseConfig);

const auth = firebase.auth();
const apiKey = firebaseConfig.apiKey;

export { auth, apiKey };

index.js

//index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./shared/App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import store from "./redux/configureStore";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

쿠키 저장하기

// Cookie.js
const getCookie = (name) => {
  let value = "; " + document.cookie;
  let parts = value.split(`; ${name}=`);

  if (parts.length === 2) {
    return parts.pop().split(";").shift();
  }
};

const setCookie = (name, value, exp = 5) => {
  let date = new Date();
  date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000);
  document.cookie = `${name}=${value}; expires=${date.toUTCString}`;
};

const deleteCookie = (name) => {
  let date = new Date("2020-01-01").toUTCString();
  document.cookie = name + "=; expires=" + date;
};

export { getCookie, setCookie, deleteCookie };

모듈 만들기

// user.js
import { createAction, handleActions } from "redux-actions";
import { produce } from "immer";
import { setCookie, getCookie, deleteCookie } from "../../shared/Cookie";
import { auth } from "../../shared/firebase";
import firebase from "firebase/app";

// initial state
const user_initial = {
  user_name: "bbakyong",
};
const initialState = {
  user: user_initial,
  is_login: false,
};

// actions
const LOG_OUT = "LOG_OUT";
const GET_USER = "GET_USER";
const SET_USER = "SET_USER";

// action creators
const logOut = createAction(LOG_OUT, (user) => ({ user }));
const getUser = createAction(GET_USER, (user) => ({ user }));
const setUser = createAction(SET_USER, (user) => ({ user }));

// middleware actions
const loginFB = (id, pwd) => {
  return (dispatch, getState, { history }) => {
    auth.setPersistence(firebase.auth.Auth.Persistence.SESSION).then(() => {
      auth
        .signInWithEmailAndPassword(id, pwd)
        .then((user) => {
          dispatch(
            setUser({
              id: id,
              user_name: user.user.displayName,
              user_profile: "",
              uid: user.user.user_uid,
            })
          );
          history.push("/");
        })
        .catch((error) => {
          var errorCode = error.code;
          var errorMessage = error.message;
          console.log(errorCode, errorMessage);
        });
    });
  };
};

const signupFB = (id, pwd, user_name) => {
  return function (dispatch, getState, { history }) {
    auth
      .createUserWithEmailAndPassword(id, pwd)
      .then((user) => {
        console.log(user);

        auth.currentUser
          .updateProfile({
            displayName: user_name,
          })
          .then(() => {
            dispatch(
              setUser({
                id: id,
                user_name: user_name,
                user_profile: "",
                uid: user.user.user_uid,
              })
            );
            history.push("/");
          })
          .catch((error) => {
            console.log(error);
          });
      })
      .catch((error) => {
        var errorCode = error.code;
        var errorMessage = error.message;
        console.log(errorCode, errorMessage);
        // ..
      });
  };
};

const loginCheckFB = () => {
  // 세션에 로그인 정보 값이 있을때 사용하는 함수.
  // 세션에 값이 있다면 이 함수로 리덕스에 값을 넣어준다.
  // 세션에 값이 없어졌다면 리덕스에서도 logOut을 이용해 값을 없애준다.
  return (dispatch, getState, { history }) => {
    auth.onAuthStateChanged((user) => {
      if (user) {
        dispatch(
          setUser({
            user_name: user.displayName,
            user_profile: "",
            id: user.email,
            uid: user.uid,
          })
        );
      } else {
        dispatch(logOut());
      }
    });
  };
};

const logoutFB = () => {
  return (dispatch, getState, { history }) => {
    auth.signOut().then(() => {
      dispatch(logOut());
      history.replace("/"); // replace: 지금 페이지와 괄호 안의 페이지를 바꿔치기 한다. 뒤로가기 해도 전 페이지가 나오지 않는다.
    });
  };
};

// reducer using handle actions, immer
export default handleActions(
  {
    [SET_USER]: (state, action) =>
      produce(state, (draft) => {
        setCookie("is_login", "success");
        draft.user = action.payload.user;
        draft.is_login = true;
      }),
    [LOG_OUT]: (state, action) =>
      produce(state, (draft) => {
        deleteCookie("is_login");
        draft.user = null;
        draft.is_login = false;
      }),
    [GET_USER]: (state, action) => produce(state, (draft) => {}),
  },
  initialState
);

const actionCreators = {
  logOut,
  getUser,
  loginFB,
  signupFB,
  loginCheckFB,
  logoutFB,
};
export { actionCreators };

사용한 컴포넌트들...

  1. 페이지 단위
    login, postdetail, postlist, postwrite, signup

  2. 중간 단위 (페이지 내부에서 영역을 나눔)
    header, post, commentlist, commentwrite

  3. 최소 단위 (거의 태그 단위)
    button, grid, image, index, input, text

좋은 웹페이지 즐겨찾기