Route와 Context API를 사용해서 로그인, 회원가입, 마이페이지 만들기

김지원·2020년 11월 24일
14

React

목록 보기
20/31

코드를 짜기 전에 생각할 것
1. 전역적으로 사용하는 상태 생각하기
2. 상태를 변경하는 함수 생각하기


페이지 개요

  • 비회원일 경우

    • 메인 페이지가 로그인 페이지
    • Context API에 있는 아이디, 비밀번호로 로그인할 시 로그인됨
    • 로그인 버튼을 눌렀을 경우 로그인됨
    • 회원가입 페이지 접근 가능
    • 회원가입 페이지에서는 아이디, 비밀번호, 비밀번호 확인을 입력받음
    • Context API로 회원정보를 저장해줌
  • 회원일 경우

    • 메인페이지에 로그아웃 버튼, 마이페이지를 들어갈 수 있는 버튼이 있음
    • 마이페이지에 접속시 로그인시 로그인한 아이디가 보임
      그리고 이 계정을 수정가능하게 함
    • 로그아웃시 login false로

디렉토리를 나누기

  • component : 공통으로 사용되는 컴포넌트들 넣기
  • context: context api
  • hooks: 커스텀 hooks 만들어 넣기
  • pages: Route에서 이동되는 페이지들 넣기
    (Login, SignUp, Home, MyPage, NotFound)

1. App.js에서 Route 설정해주기

login된 상태와 login 되지 않은 상태 두가지로 나누어 <switch>를 만들어주었다.

const App = () => {
  const isLogin = false;

  return (
    <div>
      <Header />
      {isLogin ? (
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/mypage" component={MyPage} />
          <Route path="/404" component={NotFound} />
          <Redirect from="*" to="/404" />
        </Switch>
      ) : (
        <Switch>
          <Route exact path="/" component={Login} />}
          <Route path="/signup" component={SignUp} />
          <Route path="/404" component={NotFound} />
          <Redirect from="*" to="/404" />
        </Switch>
      )}
    </div>
  );
};

2. Loign Page UI 만들기

커스텀 hooks useInput 만들기
onReset에 사용할 setValue도 return 해준다.

import React, { useCallback, useState } from "react";

export default (initialValue) => {
  const [value, setValue] = useState(initialValue);

  const handler = useCallback((e) => {
    const blank = /\s/;
    if (blank.test(e.target.value) === true) {
      alert("공백은 사용할 수 없습니다.");
      return;
    }
    setValue(e.target.value);
  }, []);

  return [value, handler, setValue];
};

Login Page UI부분

const Login = () => {
  const [id, onChangeId, setId] = useInput("");
  const [pwd, onChangePwd, setPwd] = useInput("");

  const onReset = useCallback(() => {
    setId("");
    setPwd("");
  }, [setId, setPwd]);

  const onLogin = () => {
    if (!id || !pwd) {
      alert("모든 값을 정확하게 입력해주세요");
      return;
    }

    alert("로그인");
    onReset();
  };

  return (
    <Container>
      <Title>PurpleCode</Title>
      <form>
        <InputContainer>
          <InputItem>
            <InputLabel htmlFor="user_id">아이디:</InputLabel>
            <InputDiv>
              <MyInput
                id="user_id"
                value={id}
                onChange={onChangeId}
                placeholder="아이디를 입력해주세요"
                required
              />
              <hr />
            </InputDiv>
          </InputItem>
          <InputItem>
            <InputLabel htmlFor="user_pwd">비밀번호:</InputLabel>
            <InputDiv>
              <MyInput
                id="user_pwd"
                value={pwd}
                onChange={onChangePwd}
                placeholder="비밀번호를 입력해주세요"
                required
              />
              <hr />
            </InputDiv>
          </InputItem>
        </InputContainer>
      </form>
      <LoginBtn type="submit" value="로그인" onClick={onLogin}/>
      <Link to="/signup">
        <LoginBtn type="submit" value="회원가입" />
      </Link>
    </Container>
  );
};

export default Login;

3. SignUp Page UI 만들기

  • errorMessage에 idError, pwdError, confrimPwdError를 넣어줘서 해당하는 에러가 발생할시에 error 메세지를 띄워주었다.
  • 정규표현식으로 validation을 확인해주었다.
  • useEffect를 사용하여 각각의 input 값이 바뀔때 errorMessage 상태를 변경해주었다.
const SignUp = ({ history }) => {
  const [id, onChangeId, setId] = useInput("");
  const [pwd, onChangePwd, setPwd] = useInput("");
  const [confirmPwd, onChangeConfirmPwd, setConfirmPwd] = useInput("");
  const [errorMessage, setErrorMessage] = useState({
    idError: "",
    pwdError: "",
    confirmPwdError: "",
  });
  const { idError, pwdError, confirmPwdError } = errorMessage;

  const inputRegexs = {
    idReg: /^[A-za-z0-9]{5,15}$/g,
    pwdReg: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/g,
  };
  const validationCheck = useCallback(
    (input, regex) => {
      let isValidate = false;
      if (input === "") {
        isValidate = false;
      } else if (regex.test(input)) {
        isValidate = true;
      } else {
        isValidate = false;
      }
      return isValidate;
    },
    [pwd, id]
  );

  const onReset = useCallback(() => {
    setId("");
    setPwd("");
    setConfirmPwd("");
  }, [setId, setPwd, setConfirmPwd]);
  
  /* 아이디 체크 */
  useEffect(() => {
    if (validationCheck(id, inputRegexs.idReg) || id === "") {
      setErrorMessage({
        ...errorMessage,
        idError: "",
      });
    } else {
      setErrorMessage({
        ...errorMessage,
        idError: "아이디는 영문 또는 숫자로 5~15자 이여야 합니다.",
      });
    }
  }, [id]);

  /* 비밀번호 체크 */
  useEffect(() => {
    if (validationCheck(pwd, inputRegexs.pwdReg) || pwd === "") {
      setErrorMessage({
        ...errorMessage,
        pwdError: "",
      });
    } else {
      setErrorMessage({
        ...errorMessage,
        pwdError:
          "비밀번호는 최소 하나의 문자 및 하나의 숫자로 8자 이상이여야 합니다.",
      });
    }
  }, [pwd]);
  
  /* 비밀번호 확인 체크 */
  useEffect(() => {
    if (pwd === confirmPwd || confirmPwd === "") {
      setErrorMessage({
        ...errorMessage,
        confirmPwdError: "",
      });
    } else {
      setErrorMessage({
        ...errorMessage,
        confirmPwdError: "비밀번호 확인이 일치하지 않습니다.",
      });
    }
  }, [confirmPwd]);

  const onSignUp = () => {
    if (!id || !pwd || !confirmPwd) {
      alert("모든 값을 정확하게 입력해주세요");
      return;
    }

    if (idError) {
      alert("아이디가 형식에 맞지 않습니다");
      return;
    } else if (pwdError) {
      alert("비밀번호가 형식에 맞지 않습니다");
      return;
    } else if (confirmPwdError) {
      alert("비밀번호 확인이 일치하지 않습니다.");
      return;
    }

    alert("회원 가입 완료");
    history.push("/");
    onReset();
  };

  return (
    <Container>
      <Title>회원가입</Title>
      <InputContainer>
        <InputItem>
          <InputTitle>아이디</InputTitle>
          <Input
            type="text"
            placeholder="아이디를 입력하세요"
            value={id}
            onChange={onChangeId}
            required
          />
          {idError ? <ErrorMessage>{idError}</ErrorMessage> : ""}
        </InputItem>
        <InputItem>
          <InputTitle>비밀번호</InputTitle>
          <Input
            type="password"
            placeholder="비밀번호를 입력하세요"
            value={pwd}
            onChange={onChangePwd}
            required
          />
          {pwdError ? <ErrorMessage>{pwdError}</ErrorMessage> : ""}
        </InputItem>
        <InputItem>
          <InputTitle>비밀번호 확인</InputTitle>
          <Input
            type="password"
            placeholder="비밀번호 확인을 입력하세요"
            value={confirmPwd}
            onChange={onChangeConfirmPwd}
            required
          />
          {confirmPwdError ? (
            <ErrorMessage>{confirmPwdError}</ErrorMessage>
          ) : (
            ""
          )}
        </InputItem>
      </InputContainer>
      <Btn type="submit" value="가입" onClick={onSignUp} />
      <Link to="/">
        <Btn type="submit" value="로그인" />
      </Link>
    </Container>
  );
};

export default SignUp;

4. Context 만들기

Context API를 사용하면 단 한 번에 원하는 값을 받아 와서 사용할 수 있습니다.

개념 정리

  • createContext 함수를 사용해서 새 Context를 만든다.

  • Provider를 사용하면 context의 value를 변경할 수 있다.
    value에는 상태 값뿐만 아니라 함수도 전달해 줄 수 있다.

  • Context에 있는 값을 사용할 때 Consumer 대신 useContext라는 Hook을 사용할 수 있다.
    ex) const state = useContext(UserStateContext); 하면 value 값을 사용할 수 있다.

  • useReducer: 첫 번째 파라미터에는 리듀서 함수를 넣고, 두 번째 파라미터에는 해당 리듀서의 기본값을 넣어 준다.

    이 Hook을 사용하면 state 값과 dispatch 함수를 받아 옵니다.

    • state는 현재 가리키고 있는 상태고, dispatch는 액션 발생시키는 함수입니다.
    • dispatch(action)과 같은 형태로, 함수 안에 파라미터로 액션 값을 넣어 주면 리듀서 함수가 호출되는 구조입니다.
    • useReducer에서 액션은 그 어떤 값도 사용 가능합니다.

  • 전역적으로 필요한 상태는 user

  • 회원가입의 action type은 "CREATE_USER"

  • 로그인은 "LOGIN" 로그아웃은 "LOGOUT"

  • "LOGIN" action이 들어오면 user에 userID를 넣어준다.

  • "LOGOUT" action이 들어오면 user를 다시 null로 만들어준다.

  • "CREATE_USER" action이 들어오면 userList에 user를 추가해준다.

  • reducer = 현재 상태, 그리고 업데이트를 위해 필요한 정보를 담은 action 값을 전달받아 새로운 상태를 반환하는 함수 !

UserContext.js 코드

import React, { createContext, useContext, useReducer } from "react";

const initialState = {
  userList: [],
  user: null,
};

const reducer = (state, action) => {
  switch (action.type) {
    case "CREATE_USER":
      return {
        ...state,
        userList: state.userList.concat(action.user),
      };
    case "LOGIN":
      return {
        ...state,
        user: {
          userId: action.userId,
        },
      };
    case "LOGOUT":
      return {
        ...state,
        user: null,
      };
    default:
      return state;
  }
};

const UserStateContext = createContext(null);
const UserDispatchContext = createContext(null);

export const UserProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <UserStateContext.Provider value={state}>
      <UserDispatchContext.Provider value={dispatch}>
        {children}
      </UserDispatchContext.Provider>
    </UserStateContext.Provider>
  );
};

export const useUserState = () => {
  const state = useContext(UserStateContext);
  if (!state) throw new Error("Cannot find UserProvider");
  return state;
};

export const useUserDispatch = () => {
  const dispatch = useContext(UserDispatchContext);
  if (!dispatch) throw new Error("Cannot find UserProvider");
  return dispatch;
};

index.js 코드
<UserProvider>로 감싸준다.

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
import { UserProvider } from "./context/UserContext";

ReactDOM.render(
  <UserProvider>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </UserProvider>,
  document.getElementById("root")
);

App.js 코드
const isLogin = false;
const { user } = useUserState(); 으로 변경

user가 있다면 회원의 Route로 없다면 비회원의 Route로 설정

5. 회원가입 Context API에 user 저장하기

dispatchtype "CREATE_USER"와 user객체에 id와 pwd 담아준다.

...
const signUp =({history})=>{
  
  ...
  
  const dispatch = useUserDispatch();
  
  ...
  
  onSignUp = () =>{
    ...
     dispatch({
      type: "CREATE_USER",
      user: {
        id,
        pwd,
      },
    });
    ...
  };

6. 로그인 Context API 에 있는 user라면 로그인 시키기

dispatch에 type "LOGIN"과 userID에 id값을 담아준다.

const Login = () => {
  
  ...

  const { userList } = useUserState();
  const dispatch = useUserDispatch();
  
  ...
  
  const onLogin = () => {
  ...
  
  dispatch({
      type: "LOGIN",
      userId: id,
    });
    
 ...
  }

7. 홈 화면에서 로그아웃 시키기

헤더를 만들어서

  • 로그인 했을 때는 헤더에 로고id값, 마이 페이지 버튼, 로그아웃 버튼을 보여주고,
  • 로그아웃했을 때 로그인 페이지에서는 로고회원가입페이지로 이동할 수 있는 버튼을 보여주고,
  • 회원가입 페이지에서는 로고로그인페이지로 이동할 수 있는 버튼을 보여주게 했다.

로그아웃 구현
dispatch에 type "LOGOUT"을 담아준다.

Header.js 코드

const Header = ({ location }) => {
 const { user } = useUserState();
 const dispatch = useUserDispatch();
 
 const onLogOut = () => {
   alert("로그아웃 되었습니다.");
   dispatch({
     type: "LOGOUT",
   });
 };
 return (
   <MainUl>
     <Link to="/" style={{ textDecoration: "none" }}>
       <LogoLi>PurpleCode</LogoLi>
     </Link>
     {user ? (
       <NavDiv>
         <MainLi>{user.userId}</MainLi>
         <MainLi>
           <Link to="/mypage">
             <SubmitInput type="submit" value="마이 페이지" />
           </Link>
         </MainLi>
         <MainLi>
           <SubmitInput type="submit" value="로그아웃" onClick={onLogOut} />
         </MainLi>
       </NavDiv>
     ) : location.pathname === "/signup" ? (
       <NavDiv>
         <MainLi>
           <Link to="/">
             <SubmitInput type="submit" value="로그인" />
           </Link>
         </MainLi>
       </NavDiv>
     ) : (
       <NavDiv>
         <MainLi>
           <Link to="/signup">
             <SubmitInput type="submit" value="회원가입" />
           </Link>
         </MainLi>
       </NavDiv>
     )}
   </MainUl>
 );
};

export default withRouter(Header);

Home.js 코드

const Home = () => {
  const { user } = useUserState();

  return (
    <div>
      <MainP>{user.userId}님 환영합니다.</MainP>
    </div>
  );
};

  • 맨 처음에는 userList가 빈 배열 []이고 usernull이다.
  • 회원가입을 하면 userList{id:"annie" pwd:"..."}가 추가 되었고, usernull이다.
  • 로그인을 하면 user{userId: "annie"}로 바뀌었다.

8. 마이페이지 만들기

useUserState를 통해서 {user}를 조회한다.

MyPage.js 코드

const MyPage = () => {
  const { user } = useUserState();
  return (
    <Container>
      <Title>마이 페이지</Title>
      <UserInfo>
        <Id>아이디 : </Id>
        <Value>
          {user.userId}
          <Line />
        </Value>
      </UserInfo>
      <Link to="/mypage/modify">
        <Btn type="submit" value="수정" />
      </Link>
    </Container>
  );
};

9. 정보 수정하기

UserContext에 action type "MODIFY"를 추가해서
dispatch에 type "MODIFY", 지금 로그인한 id의 userList에서 index 값과 바꾼 id, pwd 값을 담아주었다.

UserContext.js에 추가한 코드

case "MODIFY":
      state.userList.splice(action.index, 1, {
        id: action.userId,
        pwd: action.userPwd,
      });
      return {
        ...state,
        user: { userId: action.userId, userPwd: action.userPwd },
        userList: state.userList,
      };

Modify.js 코드

문제!!
Signup.js와 중복되는 부분 많다

const Modify = ({ history }) => {
  const { user, userList } = useUserState();
  const dispatch = useUserDispatch();
  const [id, onChangeId] = useInput(user.userId);
  const [nowPwd, onChangeNowPwd] = useInput("");
  const [pwd, onChangePwd] = useInput("");
  const [confirmPwd, onChangeConfirmPwd] = useInput("");
  const [errorMessage, setErrorMessage] = useState({
    idError: "",
    pwdError: "",
    confirmPwdError: "",
  });
  const { idError, pwdError, confirmPwdError, nowPwdError } = errorMessage;

  const inputRegexs = {
    idReg: /^[A-za-z0-9]{5,15}$/g,
    pwdReg: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/g,
  };
  const validationCheck = useCallback((input, regex) => {
    let isValidate = false;
    if (input === "") {
      isValidate = false;
    } else if (regex.test(input)) {
      isValidate = true;
    } else {
      isValidate = false;
    }
    return isValidate;
  }, []);

  /* 아이디 체크 */
  useEffect(() => {
    const findUser = userList.find((user) => user.id === id);

    if ((!findUser && validationCheck(id, inputRegexs.idReg)) || id === "") {
      setErrorMessage({
        ...errorMessage,
        idError: "",
      });
    } else if (findUser !== undefined) {
      setErrorMessage({
        ...errorMessage,
        idError: "이미 가입된 아이디입니다.",
      });
    } else if (!validationCheck(id, inputRegexs.idReg)) {
      setErrorMessage({
        ...errorMessage,
        idError: "아이디는 영문 또는 숫자로 5~15자 이여야 합니다.",
      });
    }
  }, [id]);

  /* 현재 비밀번호 체크 */
  useEffect(() => {
    if (user.userPwd === nowPwd || nowPwd === "") {
      setErrorMessage({
        ...errorMessage,
        nowPwdError: "",
      });
    } else {
      setErrorMessage({
        ...errorMessage,
        nowPwdError: "현재 비밀번호와 일치하지 않습니다.",
      });
    }
  }, [nowPwd]);
  /* 새 비밀번호 체크 */
  useEffect(() => {
    if (validationCheck(pwd, inputRegexs.pwdReg) || pwd === "") {
      setErrorMessage({
        ...errorMessage,
        pwdError: "",
      });
    } else {
      setErrorMessage({
        ...errorMessage,
        pwdError:
          "비밀번호는 최소 하나의 문자 및 하나의 숫자로 8자 이상이여야 합니다.",
      });
    }
  }, [pwd]);
  /* 새 비밀번호 확인 체크 */
  useEffect(() => {
    if (pwd === confirmPwd || confirmPwd === "") {
      setErrorMessage({
        ...errorMessage,
        confirmPwdError: "",
      });
    } else {
      setErrorMessage({
        ...errorMessage,
        confirmPwdError: "비밀번호 확인이 일치하지 않습니다.",
      });
    }
  }, [confirmPwd]);

  const onModify = () => {
    if (!id || !pwd || !confirmPwd || !nowPwd) {
      alert("모든 값을 정확하게 입력해주세요");
      return;
    }

    if (idError) {
      alert("아이디가 형식에 맞지 않습니다");
      return;
    } else if (nowPwdError) {
      alert("현재 비밀번호와 일치하지 않습니다.");
      return;
    } else if (pwdError) {
      alert("비밀번호가 형식에 맞지 않습니다");
      return;
    } else if (confirmPwdError) {
      alert("비밀번호 확인이 일치하지 않습니다.");
      return;
    }
    const index = userList.findIndex((x) => x.id === user.userId);

    dispatch({
      type: "MODIFY",
      index: index,
      userId: id,
      userPwd: pwd,
    });
   
    alert("수정을 완료했습니다.");
    history.push("/mypage");
  };

  return (
    <Container>
      <Title>정보 수정</Title>
      <UserInfo>
        <InfoItem>
          <InfoTitle>아이디 : </InfoTitle>
          <Value type="text" value={id} onChange={onChangeId} />
        </InfoItem>
        {idError ? <ErrorMessage>{idError}</ErrorMessage> : ""}
        <InfoItem>
          <InfoTitle>현재 비밀번호 : </InfoTitle>
          <Value type="password" value={nowPwd} onChange={onChangeNowPwd} />
        </InfoItem>
        {nowPwdError ? <ErrorMessage>{nowPwdError}</ErrorMessage> : ""}
        <InfoItem>
          <InfoTitle>새 비밀번호 : </InfoTitle>
          <Value type="password" value={pwd} onChange={onChangePwd} />
        </InfoItem>
        {pwdError ? <ErrorMessage>{pwdError}</ErrorMessage> : ""}
        <InfoItem>
          <InfoTitle>새 비밀번호 확인: </InfoTitle>
          <Value
            type="password"
            value={confirmPwd}
            onChange={onChangeConfirmPwd}
          />
        </InfoItem>
        {confirmPwdError ? <ErrorMessage>{confirmPwdError}</ErrorMessage> : ""}
      </UserInfo>
      <Btn type="submit" value="수정 완료" onClick={onModify} />
    </Container>
  );
};


알맞게 수정한 뒤

useruserList에서 test1이 annie로 바뀌었다

0개의 댓글

관련 채용 정보