심화반 - 2주차

1. Promise

: 비동기 연산이 종료된 후 결과를 알기 위해 사용하는 객체

1-1. Promise 생성

  • Promise 생성자 함수(new 키워드 사용)를 통해 생성한다.
  • 비동기 작업을 수행할 콜백함수를 전달받아 사용한다.
  • resolve : 작업이 성공한 경우 호출할 콜백함수
  • reject : 작업이 실패한 경우 호출할 콜백함수
  const promise = new Promise((resolve, reject) => {
    if(...){
      ...
      resolve("성공!");
    }else{
      ...
      reject("실패!");
    }
  });

1-2. Promise 상태값

  • pending: 비동기 처리 수행 전(resolve, reject이 아직 호출 전)
  • fulfilled: 수행 성공(resolve가 호출된 상태)
  • rejected: 수행 실패(reject가 호출된 상태)
  • settled: 성공 또는 실패(resolve나 reject가 호출된 상태

1-3. Promise 후속 처리 메소드(.then)

  • 후속 처리 메소드를 통해 비동기 처리 결과를 받아서 처리해야 한다.
  • .then(): 첫 번째 인자는 성공 시 실행, 두번째 인자는 실패 시 실행한다. 첫 번째 인자만 넘겨도 된다.
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000);
  });

  promise.then((result) => {
    console.log(result); 
  }, error => {
    console.log(error); 
  });

1-4. Promise Chaining

: 후속 처리 메소드(then)을 쭉 연결해주는 것

  • then 메소드를 체이닝해서 여러 개의 Promise를 연결해 콜백 헬 현상을 해결할 수 있다.
  • Promise Chaining은 Promise를 여러 번 사용할 수 있다.
  • 먼저 해야하는 비동기 처리를 Promise 객체로 만들어 주고 .then() 안에 차례대로 Promise로 만든 비동기 작업들을 불러준다.
  promise.then((result) => {
      console.log(result); 
  }).then((result) => {
    console.log(result);
    return "promise 2";
  }).then((result) => {
    console.log(result);
    return "promise 3";
  })

1-5. async & await

: Promise를 더 편하게 사용할 수 있는 키워드

async

  • 함수 앞에 붙여서 사용하고, 항상 Promise를 반환한다.
  async function myFunc() {
    return "프라미스를 반환해요!"; 
  }

  myFunc().then((result) => {console.log(result)});

await

  • async 없이 사용이 불가하고 async 함수 안에서만 동작한다.
  • Promise가 처리될 때(fulfilled)까지 기다렸다가 결과를 반환한다.
  • thread는 일단 비동기 작업을 위임하고 바로 다음 작업을 하는데 await는 Promise가 처리 될 때까지 기다리고 처리가 완료되면 다음 작업을 수행한다.
  • .then()이 없어도 기다렸다가 처리된다.
  async function myFunc(){
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve("완료!"), 1000);
    });

    console.log(promise); // Promise 아직 안끝남

    let result = await promise; // 여기서 기다린다는 신호를 준다.

    console.log(promise); // Promise 끝남
    console.log(result);  // 끝난 Promise의 결과 출력
  }

2. 토큰 기반 인증

: 인증 정보를 세션에 담지 않은 인증 방식

  • 사용자가 로그인을 할 때 서버에서 유저 인증 토큰을 주는데 이 토큰으로 구분한다.
  • 클라이언트에 토큰을 저장하고 서버에 토큰과 같이 요청을 보내면 서버에서 토큰으로 유저 확인 후 로그인 후에 사용가능한 기능들을 사용할 수 있다.

2-1. OAuth2.0(오어스 또는 오어터)

: 토큰을 발급하고 인증하는 방법을 관리하는 프레임워크

동작 방식

구글은 사용자의 정보를 가지고 있고 로그인 검증도 해주기 때문에 Resource Server(자원 서버) + Authorization server(권한 서버)이다.

① 구글이 로그인할 때 사용자가 입력한 정보를 보고 client에 접근 권한을 부여한다.
② client는 권한을 가지고 Authorization server(권한 서버)에 access_token을 요청한다.
③ Authorization server(권한 서버)는 client가 보낸 accss_token을 통해 권한을 확인하고 결과를 client에 보낸다.

Refresh token

  • access_token이 만료되었을 때 구글에 다시 token을 요청할 수 있다.
  • access_token보다 만료일이 더 길다.

2-2. JWT(Json Web Token)

: JSON 형태로 이루어진 토큰

  • 전자서명이 포함되어 있어 보안이 좋다.

[header].[payload(내용)].[signature(서명)]

  • header: 토큰의 타입, 암호화 방식 정보
  • payload: name:value의 한 쌍으로 담긴 정보
  • signature: secret key를 포함하고 header와 payload 정보가 암호화된 서명 정보

동작 방식

  • 토큰 기반 동작 방식 그대로 움직인다.
    ① server에서 요청 확인 후 secret key를 가지고 access_token을 발급한다.
    ② client에 JWT를 전달한 후 다시 client는 Authorization header에 JWT를 담아서 보낸다.
    ③ server에서 JWT의 서명을 확인하고 payload에서 정보를 확인한 후 API 응답을 보낸다.

3. 웹 저장소

  • access_token을 저장할 수 있는 클라이언트 저장소는 아래와 같다.

3-1. Cookies

  • key:value 형태의 저장소
  • 사이트에 접속한 후에 열어야 cookies의 사용이 가능하다.
  • 4KB의 저장공간을 가지고 있다.

Cookies 사용법

document.cookie = "MY_COOKIE=here;";  // Cookies 만들기
console.log(document.cookie); // Cookies 가져오기
document.cookie = "MY_COOKIE=here; expires=new Date('2020-12-12').toUTCString()"; // Cookies 삭제

3-2. Session Storage

  • key:value 형태의 저장소
  • 브라우저를 닫으면 저장된 데이터는 삭제된다. 다음 브라우저를 열었을 때에도 유지해야 하는 데이터는 넣기 힘들다.
sessionStorage.setItem("MY_SESSION", "here"); // 추가하기
sessionStorage.getItem("MY_SESSION"); //가져오기
sessionStorage.removeItem("MY_SESSION"); //삭제하기

3-3. Local Storage

: key:value 형태의 저장소

  • 따로 지우지 않는 한 계속 브라우저에 데이터가 남아있어 보안상 취약해지기 쉽다. 사용자의 중요한 정보는 저장하지 않는게 좋다.
  • 5MB의 저장공간을 가지고 있다.
localStorage.setItem("MY_LOCAL", "here"); // 추가하기
localStorage.getItem("MY_LOCAL"); //가져오기
localStorage.removeItem("MY_LOCAL"); // 삭제하기

4. 쿠키 저장

  • 로그인 페이지에서 id와 password를 입력하면 Cookies로 저장한다.
  • Cookies 저장소 사용법을 이용해서 Cookies 값을 가져오고, 저장하고, 삭제하는 함수를 만든다.
  • split(): 매개변수를 중심으로 문자열을 나누어 준다.
  • pop(): 배열의 마지막 요소를 제거한다.
  • shift(): 배열의 첫 번째 요소를 제거한다.
  • toUTCString(): 표준 시간대를 사용한 날짜를 문자열로 변환한다.
  // cookie 가져오기
  const getCookie = (name) => {
    let value = '; ' + document.cookie;
    let parts = value.split(`; ${name}=`);

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

  // cookie 추가하기
  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()}`;
  }

  // cookie 삭제하기
  const deleteCookie = (name) => {
     let date = new Date('2021-01-01').toUTCString();
     console.log(date);
     document.cookie = name + '=; expires=' + date;
  }

  export { getCookie, setCookie, deleteCookie };

5. 로그인하기(1) - Header 분기, Redux

5-1. Header 컴포넌트 분기

  • 로그인을 했을 때와 하지 않았을 때의 Header 컴포넌트가 바뀌어야 한다.
  • useEffect Hook을 이용해 cookie가 있을 때와 없을 때의 Header 컴포넌트를 변경한다.
  const [ is_login, setIsLogin ] = useState(false);

  useEffect(() => {
    let cookie = getCookie('user_id');
    
    if(cookie){
      setIsLogin(true);
    } else {
      setIsLogin(false);
    }
  })

  if(is_login){
    return (
      <React.Fragment>
        <Grid is_flex padding='4px 16px'>
          <Grid>
            <Text size='24px' bold margin='0px'>Hello</Text>
          </Grid>
          <Grid is_flex>
            <Button text='내정보'></Button>
            <Button text='알림'></Button>
            <Button text='로그아웃' onClick={() => deleteCookie('user_id')}></Button>
          </Grid>
        </Grid>
      </React.Fragment>
    );
}

로그인 했을 때 Header

로그아웃 했을 때 Header

5-2. Redux 설치

  • 페이지마다 쿠키를 체크하기가 어렵기 때문에 Redux를 사용해서 로그인 상태를 저장하고 어떤 컴포넌트에서든지 편하게 볼 수 있도록 한다.
  • Redux, 경로 이동까지 해줄 History, Router와 History를 이어줄 모듈 설치
  • 불변성을 관리하는 immer, 액션을 관리하는 Redux-actions 설치

6. 로그인하기(2) - 유저 모듈 만들기

6-1. import

  • creatAction, handleActions는 action과 reducer를 편하게 사용할 수 있도록 도와준다.
  import { createAction, handleActions } from 'redux-actions';
  import { produce } from 'immer';

6-2. Action Type 만들기

  const LOG_IN = 'LOG_IN';
  const LOG_OUT = 'LOG_OUT';
  const GET_USER = 'GET_USER';

6-3. Action 생성 함수 만들기

  • Action 생성 함수가 있어야 Action 객체를 만들어 사용이 가능하다.
  const logIn = createAction(LOG_IN, (user) => ({user}));
  const logOut = createAction(LOG_OUT, (user) => ({user}));
  const getUser = createAction(GET_USER, (user) => ({user}));

6-4. initialState

  const initialState = {
    user: null,
    is_login: false,
  }

6-5. immer를 사용한 Reducer

  • immer로 원본 값(initialState)을 복사한 값을 draft를 통해 받아온다.
  • draft가 불변성을 유지해서 바꿔준다.
  • payload에 보낸 데이터가 담겨있다. payload.user를 해야 action을 통해 넘겨주었던 값을 가져올 수 있다.
  export default handleActions({
    [LOG_IN]: (state, action) => produce(state, (draft) => {
      setCookie('is_login', 'succes');
      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);

6-6. actionCreator

const actionCreators = {
  logIn,
  getUser,
  logOut,
};
export { actionCreators };

7. 로그인하기(3) - Redux Store 만들기

① combineReducers()를 사용해서 export한 reducer를 모아 root reducer를 만든다.
② 미들웨어를 적용해주고
③ createStore()를 사용해서 root reducer와 미들웨어를 엮어 스토어를 만든다.

7-1. import

  import { createStore, combineReducers, applyMiddleware, compose } from "redux";
  import thunk from "redux-thunk";
  import { createBrowserHistory } from "history";
  import { connectRouter } from "connected-react-router";
  // reducer
  import User from "./modules/user";

7-2. root reducer

  • combineReducers()
  const rootReducer = combineReducers({
    user: User,
  });

7-3. Middlewares

: 개발할 때 편하기 위해서 사용하기 때문에 개발환경일 때에만 사용한다.

  • env: 어느 개발환경인지 알려준다. 배포를 한다면 배포환경을 알려준다.
  • require: 패키지를 사용하려고 가져올 때 사용한다.
  • logger: 콘솔에 이전 상태, 이후 상태가 찍힌다. 데이터 안에 담긴 값, 어떤 액션이 일어나서 변경된 값을 콘솔에 찍힌다.
  const middlewares = [thunk];
  const env = process.env.NODE_ENV;

  // 개발환경에서는 로거라는 걸 하나만 더 써볼게요.
  if (env === "development") {
    const { logger } = require("redux-logger");
    middlewares.push(logger);
  }

7-4. Redux devTools

  • 브라우저일 때에만 돌려주고 devTools가 설치되어 있으면 열어준다.
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;

7-5. Middlewares 묶기

  • applyMiddleware로 지금까지 있었던 모든 미들웨어들을 사용한다고 알려주고 composeEnhancers()를 사용해서 묶어준다.
const enhancer = composeEnhancers(applyMiddleware(...middlewares));

7-6. Store 만들기

  • initialStore를 받아서 createStore로 미들웨어를 엮어서 Store를 만든다.
  let store = (initialStore) => createStore(rootReducer, enhancer);
  export default store();

7-7. history

  • history 객체를 생성하고 생성한 history를 라우터와 연결되면 store에 저장된다.
  • withExtraArgument: 다른 인수를 더 넘겨준다.
  export const history = createBrowserHistory();

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

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

8. 로그인하기(4) - Store 주입

8-1. Store 주입

index.js

  • Provider를 이용해서 Store를 주입한다.
  import { Provider } from 'react-redux';
  import store from './redux/configureStore';

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

App.js

  • ConnectedRouter를 이용해서 Redux랑 같은 history를 사용하면서 공유한다.
  import { ConnectedRouter } from 'connected-react-router';
  import { history } from '../redux/configureStore';

  return (
    <ConnectedRouter history={history}>
      <Route path="/" exact component={PostList} />
      <Route path="/login" exact component={Login} />
      <Route path="/signup" exact component={Signup} />
    </ConnectedRouter>
  );

8-2. Redux Hook

  • useSelector: 상태를 조회한다.
  • useDispatch: 생성한 dispatch를 발생한다.

Login.js

  import {actionCreators as userActions} from "../redux/modules/user";
  import { useDispatch } from "react-redux";

  const login = () => {
    dispatch(userActions.login({user_name: "perl"}));
  }

user.js

  const loginAction = (user) => {
    return function (dispatch, getState, {history}) {
      dispatch(logIn(user));
      history.push('/');
    }
  }

...

  const actionCreators = {
    logIn,
    getUser,
    logOut,
    loginAction,
  };

header.js

  const dispatch = useDispatch();
  const is_login = useSelector((state) => state.user.is_login);

  <Button text="로그아웃" _onClick={() => {dispatch(userActions.logOut({}));}}></Button>

9. 회원가입 구현

9-1. auth.createUserWithEmailAndPassword()

: 신규 사용자가 자신의 이메일 주와 비밀번호를 사용해 가입할 수 있도록 도와주는 firebase 함수

  • 양식을 작성 후 이메일 주소와 비밀번호의 유효성 검사를 한 후 메소드에 전달한다.

9-2. auth.currentUser.updateProfile()

: 사용자의 기본 프로필 정보를 업데이트할 때 사용하는 메소드

  • 이메일과 비밀번호 외에도 사용자의 닉네임을 업데이트하기 위해서 사용한다.
  • 닉네임까지 변경하고 로그인 처리가 완료되면 Redux에 저장한 후 history.push를 통해 메인 페이지로 이동한다.
  • firebase에 로그인이 되어있는 상태인데도 redux에 로그인 상태 정보가 없기 때문에 로그인 하기 전 Header로 변경되는 것이다.
  auth.currentUser.updateProfile({
    displayName: user_name,
  }).then(()=>{
    dispatch(setUser({user_name: user_name, id: id, user_profile: ''}));
    history.push('/');
  }).catch((error) => {
    console.log(error);
  });

10. 로그인 구현

10-1. auth.signInWithEmailAndPassword()

: 비밀번호를 통해 로그인을 처리하는 firebase 함수

  • 사용자가 로그인을 하면 이메일 주소와 비밀번호를 메소드에 전달한다.
  • 로그인 완료 후 메인 페이지로 이동한다.

10-2. type

  • 현재 비밀번호를 입력하면 문자들이 보이는데 Input elements에서 type을 넘겨주어 * 로 보이도록 한다.
  Input.defaultProps = {
    label: "텍스트",
    placeholder: "텍스트를 입력해주세요.",
    type: "text",
    _onChange: () => {},
  };

11. 로그인 유지

  • 페이지가 새로고침 되었을 때에도 로그인 유지를 해야한다.

11-1. auth.setPersistence()

: 메소드를 호출하면 기존의 지족성을 유지하거나 수정할 수 있는 함수

11-2. firebase.auth.Auth.Persistence.SESSION

: 현재의 session이나 탭에서만 상태가 기록하고 유지되고 인증된 탭이나 창을 닫으면 정보가 삭제된다.

  firebase.auth().setPersistence(
    firebase.auth.Auth.Persistence.SESSION
  ).then(() => {});

11-3. auth.onAuthStateChanged()

: 로그인이 되면 firebase에서 자체적으로 저장해 놓은 정보를 인증 함수를 통해 값을 가져와 Redux에 넣어주는 로그인 체크 함수
① 시작점은 App.js에서 Session Check를 먼저 한다.
② Session이 있으니 Redux로 로그인 상태를 보낸다.
③ 로그인 정보의 유무를 확인한 후 사용자의 정보를 넣어준다.

  const _session_key = `firebase:authUser:${apiKey}:[DEFAULT]`;
  const is_session = sessionStorage.getItem(_session_key)? true : false;

  React.useEffect(() => {

    if(is_session){
      dispatch(userActions.loginCheckFB());
    }

  }, []);

12. 로그아웃 구현

12-1. auth.signOut()

: firebase에서 제공하는 로그아웃 함수

replace()

: 새로운 문자열을 반환하는 함수

  • 로그아웃을 했을 때 뒤로가기를 한다고 해서 다시 들어가게 해주면 안된다.
  • 현재 페이지와 replace() 안의 주소와 바꿔주면 뒤로가기 해도 원래 페이지가 나오지 않는다.
  auth.signOut().then(() => {
    dispatch(logOut());
    history.replace("/");
  });

13. 아이디 이메일 Check

13-1. 정규 표현식

: 문자를 조합해서 비교할 때 사용된다.

  • 정규식 패턴
  • ^[0-9a-zA-Z] : 첫 글자가 0~9거나 영문 대소문자이다.
  • ([-_.0-9a-zA-Z])*: -_. 특수문자와 ()를 사용해 숫자나 영문자가 여러 개 들어올 수 있다.
  • ([a-zA-Z])*: . 뒤에는 문자만 들어오도록 한다.
  • _reg.test(email): 정규식 패턴에 맞는지 확인한다.
let _reg = /^[0-9a-zA-Z]([-_.0-9a-zA-Z])*@[0-9a-zA-Z]([-_.0-9a-zA-z])*.([a-zA-Z])*/;

_reg.test(email)

14. Post 상세 페이지

  • 주소에 post id를 주면 좋다.
  <Route path="/post/:id" exact component={PostDetail} />

좋은 웹페이지 즐겨찾기