이제 토큰을 받아올 준비가 됐으니 로그인 페이지를 만들어 POST를 하고 아이디, 비밀번호를 입력해 버튼을 누르면 토큰을 받아 로컬스토리지에 넣는 방식까지 만들어 보겠다.
먼저 내 상황을 말해보겠다.
이런 상태였다. 2번에서 미리 만들어 놓은 상태이긴 하지만 라우팅, 유효성 검사, 버튼 기능 X, POST API 등 부족한 것이 많았다. 완전 초기 상태.
import React, { useState } from "react";
import { Text, Margin } from "@/src/components/ui";
import styled from "styled-components";
import router from "next/router";
import Image from "next/image";
const Style = {
Header: styled.div`
width: 200px;
margin: 0 auto;
padding: 48px;
`,
AccountWrapper: styled.div`
padding-bottom: 30px;
`,
InputField: styled.div`
margin: 0 auto;
width: 452px;
height: 50px;
border: 1px solid #dcdce0;
border-radius: 8px;
margin-bottom:18px;
& > input {
border: none;
padding-left: 16px;
padding: 14px;
border-radius: 8px;
height: 22px;
&::placeholder {
font-weight: 400;
font-size: 16px;
line-height: 140%
color: #959599;
}
}
`,
ButtonWrapper: styled.div`
text-align: center;
`,
LoginButton: styled.button`
width: 452px;
height: 50px;
background-color: #eeeef2;
color: #959599;
border-radius: 8px;
padding-botton: 24px;
font-weight: 700;
font-size: 16px;
line-height: 22.4px;
`,
RegisterButton: styled.button`
width: 56px;
margin: 20px;
border-bottom: 1px solid #49494d;
color: #49494d;
font-weight: 800;
font-size: 16px;
line-height: 20px;
`,
};
export default function Login() {
const [email, setEmail] = useState("");
return (
<>
<Style.Header>
<Image src="auth/loginLogo.svg" alt="logo" />
<Margin direction="column" size={8} />
<Text.Body6 color="gray700">다함께 즐기는 국제교류 커뮤니티</Text.Body6>
</Style.Header>
<Style.AccountWrapper>
<Style.InputField>
<input type="email" placeholder="이메일"></input>
</Style.InputField>
<Style.InputField>
<input type="password" placeholder="비밀번호"></input>
</Style.InputField>
</Style.AccountWrapper>
<Style.ButtonWrapper>
<Style.LoginButton>로그인</Style.LoginButton>
<Style.RegisterButton onClick={() => router.push("auth/SignupPage")}>
회원가입
</Style.RegisterButton>
</Style.ButtonWrapper>
</>
);
}
초기 코드를 보면 알겠지만 간단한 CSS만 구현이 되어 있고, 기능면은 아예 없다. 이제 순서대로 훑어 내려가보자
먼저 지금 팀에서 쓰는 문법 및 작성 방법을 통일해야한다. 세부 내용은 개발환경 세팅에 있다.
import React, { useState } from "react";
import styled from "styled-components";
import Image from "next/image";
import router from "next/router";
import { Text, Margin } from "@/src/components/ui";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = () => {
};
const handleSignup = () => {
router.push("/auth/signup");
};
return (
<>
<S.Header>
<Image src="/auth/loginLogo.svg" alt="logo" width={200} height={200} />
<Margin direction="column" size={8} />
<Text.Body6 color="gray700">다함께 즐기는 국제교류 커뮤니티</Text.Body6>
</S.Header>
<S.AccountWrapper>
<S.InputField>
<input
type="email"
placeholder="이메일"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</S.InputField>
<S.InputField>
<input
type="password"
placeholder="비밀번호"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</S.InputField>
</S.AccountWrapper>
<S.ButtonWrapper>
<S.LoginButton onClick={handleLogin}>로그인</S.LoginButton>
<S.RegisterButton onClick={handleSignup}>회원가입</S.RegisterButton>
</S.ButtonWrapper>
</>
);
}
const S = {
Header: styled.div`
width: 200px;
margin: 0 auto;
padding: 48px;
`,
AccountWrapper: styled.div`
padding-bottom: 30px;
`,
InputField: styled.div`
margin: 0 auto;
width: 452px;
height: 50px;
border: 1px solid #dcdce0;
border-radius: 8px;
margin-bottom: 18px;
display: flex;
align-items: center;
& > input {
border: none;
padding: 0 16px;
border-radius: 8px;
height: 100%;
flex: 1;
&::placeholder {
font-weight: 400;
font-size: 16px;
line-height: 140%;
color: #959599;
}
}
`,
ButtonWrapper: styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`,
LoginButton: styled.button`
width: 452px;
height: 50px;
background-color: #eeeef2;
color: #959599;
border-radius: 8px;
margin: 0 auto;
font-weight: 700;
font-size: 16px;
line-height: 22.4px;
`,
RegisterButton: styled.button`
margin: 20px;
width: 56px;
border-bottom: 1px solid #49494d;
color: #49494d;
font-weight: 800;
font-size: 16px;
line-height: 20px;
cursor: pointer;
`,
};
큰 수정은 Next.js에서 img
태그는 전부 Image
컴포넌트를 사용해야하니 그렇게 바꿨고, 기본적인 라우팅, password
상태, 리액트쿼리로 통신할 함수 handleLogin
을 추가해 넣었다.
이제 개발환경과 비슷하게 했고, 로그인 데이터 담을 그릇은 구현했으니 로그인 관련 기능을 구현할 차례다.
기획/디자인 조건중에 로그인 버튼을 조절하는 기능도 있다.
이렇기 때문에 disabled라는 속성을 버튼에 따로 만들어서 조절해줘야한다.
<S.ButtonWrapper>
<S.LoginButton disabled={isButtonDisabled} type="submit">
로그인
</S.LoginButton>
<S.RegisterButton type="button" onClick={handleSignup}>
회원가입
</S.RegisterButton>
</S.ButtonWrapper>
const S = {
...
LoginButton: styled.button`
width: 452px;
height: 50px;
background-color: ${({ disabled }) => (disabled ? "#eee" : "#ff812e")};
color: ${({ disabled }) => (disabled ? "#959599" : "#fff")};
border-radius: 8px;
margin: 0 auto;
font-weight: 700;
font-size: 16px;
line-height: 22.4px;
cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")};
`,
...
}
이렇게 말이다. 이제 disabled
에서 받는 불린데이터 값으로 조절이 가능해졌다.
isButtonDisabled
를 만들어보자.
const isButtonDisabled =
email.length < 8 || !email.includes("@") || !email.includes(".") || !password;
일단 간단하게 바로 불린값을 받는 원시데이터 늒임으로 만들어봤다. 나중에 디자인이 변경된다고 해서 만약 에러메세지가 어떻게 나올지 정해진다면 isButtonDisabled
가 불린 데이터 상태가 될 것이고, set
을 통해 onError
같은 걸로 조절할 터이니 임시로 이렇게 만들었다.
POST API 호출 관련 코드를 구현해보자.
명세서를 기반으로 1.0 편에서 instance
를 이용하여 만들어서 호출하고, 리액트쿼리를 사용해 mutate해보자!
요청과 응답으로 오는 데이터를 간단하게 표시하겠다.
email
string, 이메일password
string, 비밀번호grantType
string, 인증 접두사accessToken
string, access tokenrefreshToken
string, refresh token keyadmin
boolean, 관리자 여부 - true면 관리자, false면 일반 유저이렇게 받아주면 된다. 이를 토대로 API 코드를 작성해보자.
/api/auth/LoginAPI.ts
라는 경로에 만들었다.
import instance from "../axiosModul";
export const postLogin = async (email: string, password: string) => {
const response = await instance.post("/auth/login", { email, password });
return response.data;
};
겁나 간단하지 않는가? 이것이 바로 instance
의 힘이다.
postLogin
이라는 함수를 만들고 파라미터로 둘다 string
인 email
, password
로 들어오게끔 만든다./auth/login
도메인에 POST 요청하는 코드를 만들고, response
의 data
로 리턴하게끔 해줬다.import { useMutation } from "react-query";
로 불러와서 mutate해보자!
const { mutate: login, isLoading } = useMutation(() => postLogin(email, password), {
onSuccess: (res) => {
console.log(`${res.message} ${res.data.refreshToken}`);
// 액세스토큰 리프레쉬 토큰 로컬스토리지 넣기, admin에 따라 라우팅
const { refreshToken, accessToken, admin } = res.data;
saveRefreshTokenToLocalStorage(refreshToken);
saveAccessTokenToLocalStorage(accessToken);
// router.push(admin ? "/admin" : "/main");
},
onError: (error) => {
console.log(`로그인 실패! ${error}`);
// 로그인 실패시 에러 메시지 출력
alert("로그인 실패! 이메일과 비밀번호를 확인해주세요.");
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
login();
};
useMutation
을 만들어 postLogin(email, password)
을 받아준다.onSuccess
시 토큰이 반환되니 토큰을 가져와 로컬스토리지에 저장. 참고로 관리자 계정이면 admin
으로 라우팅되게 설정onError
시 임시로 alert
되게 만들었다. 나중에 에러 메세지 디자인되면 저것에 맞게 만들것이다.handleSubmit
으로 mutate
인 login()
가 실행되게 설정이런 식으로 했다!
유효성 검사로 조건이 맞춰지지 않으면 로그인이 disabled
였다가 활성화가 된다. 또 로그인 되면 로컬스토리지에 잘 들어간다!
나중에 에러처리로 에러 메세지 및 로그인 버튼 비활성화되게 만들게 수정할 것이다. 그때 다시 보자! 끝!
mutate: login 이부분이 인상 깊네요 이런식으로 활용할 수 있는지 몰랐어요 ㅎㅎ 열심히 쓰셨네요 고생하셨습니다.