📌 Kream
프로젝트 기간
22.06.07 ~ 22.06.17
개발 인원
Front-end 4명, Back-end 1명
필수구현
Nav Bar & Footer ✔
소셜 로그인 & 회원가입 ✔
상품 페이지
상품 상세
매매입찰
📌 focus
2차 프로젝트는 아래 내용들을 목표로 진행되었다.
- 1차 프로젝트시 각자 해보지 못한 기능들을 구현
- 스프린트 일정에 맞는 작업 진행과 속도 개선
- 프론트엔드와 백엔드 간의 커뮤니케이션 개선
- 라이브러리 연습
Front-end
: HTML/CSS, JavaScript, React.js, React-Router, Styled-Component, AWS(EC2)
Back-end
: Django, Python, MySQL, AWS(RDS, EC2), Bcrypt, JWT, django-cors
nav, footer & login page
✍️ 1차때 로그인 페이지를 작성하는 동기를 보고 나도 백엔드와 협업을 통해서 로그인 페이지를 만들어보고 싶다고 생각했었다.
2차에는 소셜로그인 기능을 사용해야 했는데, 직접 해보고 싶다고 의견을 냈고 팀원들의 배려로 해당 페이지를 맡을 수 있게되었다. 😀
React와 JSX는 class와 for를 제외하면 표준 HTML 속성을 모두 사용할 수 있다.
class와 for 대신 각각 className
과 htmlFor
를 사용한다.
인풋 값에 placeholder
적용 및 포커스 했을 때 placeholder
안 보이도록 설정하기
const Input = styled.input`
border: none;
border-bottom: 1px solid #ebebeb;
padding: 10px 0;
font-size: 14px;
&:focus {
outline: none;
::placeholder {
opacity: 0;
}
}
::placeholder {
color: #bbb;
opacity: 1;
}
`;
✍️ form
과 label
과 input
부분에서 label
의 for
을 htmlFor
로 작성해야하는 것을 알았다.
팀마다 선호하는 방식이 다르다고 한다.
const LoginBtn = styled.button`
background: #ebebeb;
width: 100%;
margin: ${props => props.theme.fontSizes.xl};
padding: ${props => props.theme.paddings.large};
border: none;
border-radius: 0.625rem;
font-size: ${props => props.theme.fontSizes.small};
color: ${props => props.theme.colors.white};
cursor: pointer;
`;
위 코드처럼 props를 일일이 받아올 수 있고
${({ theme }) => `
font-size: ${theme.fontSizes.small}
color: ${theme.colors.white};
`}
구조분해 할당으로 모아서 쓸 수있다. 이 경우 함수 밖과 안에서 선언된 스타일의 순서 컨벤션은 각각 맞춰주면 된다.😀
❓리액트 아이콘즈에서 받아온 아이콘 컴포넌트는 어떻게 스타일을 줄 수 있을까🤔
<FooterIcons>
{/* 각각의 아이콘(컴포넌트)는? */}
<FaRegEnvelope />
<FaInstagram />
<FaBold />
</FooterIcons>
const Footer = () => {
return (
<FooterContainer>
<FooterIcons>
<MessageIcon />
<InstaIcon />
<FaBold />
</FooterIcons>
<Contents>이용안내</Contents>
<Contents>고객지원</Contents>
<Contents>고객센터 1588-7813</Contents>
<ContentDetails>
...
const MessageIcon = styled(FaRegEnvelope)`
margin-right: 10px;
`;
const InstaIcon = styled(FaInstagram)`
margin-right: 10px;
`;
✍️ 새로 알게된 점
→ 이경우 리액트아이콘에서 이 기능을 지원해 줄 경우에만 가능하다고 한다.
✨ 리팩토링 (네스팅을 받아서 내부에서 사용하기)
const FooterIcons = styled.div`
svg {
margin-right: ${props => props.theme.margins.large};
margin-bottom: ${props => props.theme.margins.xxl};
font-size: ${props => props.theme.fontSizes.xxl};
&:last-child {
margin-right: 0;
}
&:hover {
cursor: pointer;
}
}
✍️ 새로 알게된 점
1) import 로 {css} 로 가져오면
스타일을 줄때 내가 정의한 css 속성으로 mixin 하듯이 가져다 쓸 수 있다. ( 일종의 스타일드 컴포넌트에서 제공되는 기능중 하나인 것 같다. )
→ 쓰는 이유중 하나는 theme에서 저장을 미리 못했을때나, 내 컴포넌트에서 자주 쓰일 경우 형식을 지정해두고 가져다 쓰기 좋기 때문이라고 한다 :)!
2) 스타일드 컴포넌트로 인해서 랜덤 클래스명을 확인하기 힘들때 import 부분에 styled-components/macro 를 적어주면 해당 컴포넌트의 클래스네임은 기존 값으로 볼 수 있는 듯 하다.🧐
애플리케이션 추가
동의항목 - 개인정보 동의 파트
카카오 로그인
1) 활성화 설정을 켜면 카카오 로그인을 사용할 수 있다.
2) Redirect URI
3) 동의창 미리보기로 실제 사용자가 보게 될 카카오 로그인 동의 화면을 확인할 수 있다.
✨ 이렇게 기본 셋팅 완료! 이제 통신과정을 이해하고 백엔드 팀원과 함께 통신해보자!
📌 방법 1)
const Redirect = () => {
const url = new URL(window.location.href);
const code = url.searchParams.get('code');
const getToken = async () => {
await fetch(`${API.KAKAO_TOKEN}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
},
body: `grant_type=authorization_code&client_id=${REST_API_KEY}&code=${code}&redirect_uri=${REDIRECT_URI}`,
}).then(res => {
console.log(res.json());
});
};
useEffect(() => {
code && getToken();
}, [code]);
❗️ 작성하니 아래와같은 에러가 발생했다.
→ 인가가 중복적으로 사용되고 있어서 그렇다. (처음 발급시에는 200, 새로고침하면 에러)
✍️ 해결과정
→ 먼저 에러의 이유를 알아보기 위해 res.json()을 콘솔에 찍어서 error_code 를 확인(이전에는 console.log(res)로 찍어서 그 값만 확인하느라 에러코드를 보지 못했다.🥲)
→ 에러 코드 KOE 30 확인 후 카카오 API명세서에 기재된 에러 사항을 확인
→ 인가 코드 중복 사용과 관련된 문제임을 확인한 후 해결할 수 있었다.
//LoginData.js
export const REST_API_KEY = `...`;
export const REDIRECT_URI = `http://localhost:3000/users/signin/kakao/callback`;
export const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}`;
// Redirect.js
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { API } from '../../config';
import { REST_API_KEY, REDIRECT_URI } from './login/LoginData.js';
const getToken = async () => {
await fetch(`${API.KAKAO_TOKEN}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
},
body: `grant_type=authorization_code&client_id=${REST_API_KEY}&code=${code}&redirect_uri=${REDIRECT_URI}`,
})
.then(res => res.json())
.then(data => localStorage.setItem('kakao_token', data.access_token));
sendToken();
};
const sendToken = async () => {
await fetch(`${BASE_URL}/users/login/kakao`, {
method: 'GET',
headers: { Authorization: localStorage.getItem('kakao_token') },
})
.then(res => res.json())
.then(data => {
localStorage.setItem('cream_token', data.cream_token);
navigate('/');
});
};
useEffect(() => {
getToken();
}, []);
return <div>redirect.js 컴포넌트 화면</div>;
};
export default Redirect;
❗️ 방법1의 경우
→ 이렇게 되면 로컬스토리지에 두 개의 토큰이 저장되는 문제가있고, 카카오서버로부터 받은 액세스 토큰에서 어떤 문제가 발생될지 모르므로 보완이 필요하다는 조언을 받았다.
✍️ → 추후 질문을 통해서 알게된 점 ✨
카카오 서버로부터 받아온 액세스 토큰을 굳이 로컬스토리지에 저장하지 않고 바로 fetch 함수 로직 내에서 바로 서버로 넘겨준다. → 로컬스토리지에 저장되는 토큰은 마지막 절차에서 백엔드로부터 받는 우리사이트 토큰 하나로, 이전과 같이 잘 작동 된다는 것을 알게되었다.😀 )
📌 방법 2)
인가 코드를 받아서 백엔드 서버에 넘겨주고 백엔드쪽에서 액세스 토큰을 받은 후 서버 전용 토큰 발행, 그 토큰을 프론트에게 넘겨주는 로직으로 수정했다.
이럴경우 최종 사이트 토큰만 로컬스토리지에 저장하면 된다. (위 사진 단계와 동일한 흐름으로 전개된다.)
const Redirect = () => {
const url = new URL(window.location.href);
const code = url.searchParams.get('code');
const navigate = useNavigate();
const getToken = async () => {
await fetch(`${API.KAKAO_LOGIN}?code=${code}`, {
method: 'GET',
})
.then(res => res.json())
.then(data => {
localStorage.setItem('cream_token', data.cream_token);
navigate('/');
});
};
useEffect(() => {
getToken();
}, []);
return <div>redirect.js 컴포넌트 화면</div>;
};
사이트 전용 토큰을 가져와서 스토리지에 저장을 해주려는데 키는 잘 떴지만, value가 undefined로 계속 처리되는 것을 확인했다. 서버쪽엔 200으로 잘 뜨는데 ..😧?
넘겨주는 값에 문제가 있는 것 같아서 이것저것 확인해 보다가 json() 처리를 안하고 res 그대로 넘겨주고 있는 것을 확인, .then(res => res.json())
을 하고 데이터를 받아와서 로컬스토리지에 저장했더니 잘 나오게 되었다.👍
이과정에서 json() 를 하지 않은 값과 처리를 한 값 모두 콘솔에 출력해 보았다.
(1) res만 출력
(2) res.json() 출력
const [isLogin, setIsLogin] = useState(false);
const navigate = useNavigate();
const creamToken = localStorage.getItem('cream_token');
useEffect(() => {
creamToken && setIsLogin(true);
}, [creamToken]);
const goToMypage = () => {
creamToken
? fetch(`${API.USERS}`, {
method: 'GET',
headers: {
Authorization: creamToken,
},
})
.then(res => {
if (res.ok) {
return res.json();
}
})
.then(() => {
navigate('/마이페이지');
})
: navigate(`/login`);
};
return (
...
{isLogin ? (
<Button
onClick={() => {
goToMypage();
}}
>
MYPAGE
</Button>
) : (
<Button login>LOGIN</Button>
)}
const sendCodeToBack = () => {
const option = {
url: `${API.KAKAO_LOGIN}?code=${accessCode}`,
method: 'GET',
};
axios(option).then(res => {
if (res.status === 200) {
localStorage.setItem(USER_TOKEN, res.data.token);
navigate('/');
} else {
navigate('/login');
alert('로그인이 실패하였습니다.');
}
});
};
const [isLogin, setIsLogin] = useState(false);
const creamToken = localStorage.getItem('access_token');
const navigate = useNavigate();
useEffect(() => {
creamToken && setIsLogin(true);
}, [creamToken, isLogin]);
const logout = () => {
localStorage.removeItem('access_token');
alert('로그아웃 되었습니다');
setIsLogin(false);
};
...
localStorage.clear();
로 로컬스토리지를 비웠으나 localStorage.removeItem('access_token')
로 키에 해당하는 값만 삭제하는 것으로 수정했다.;
가 있는지, ' or "
처리가 잘 되었는지 확인하기.아쉬웠던 점
: 카카오 API 공식문서를 더 끈질기게 정독 했으면 어떨까 하는 아쉬움과 조금 더 욕심내서 다른 소셜 로그인도 구현 해 보았으면 좋았을 것 같았다.
프론트단에서 카카오서버로부터 액세스토큰까지 받는과정과 인가코드까지만 받고 백엔드로 넘겨주는 과정 모두 경험해 볼 수 있어서 더 기억에 남았다.
로그인 통신 구현을 통해서 프론트와 백엔드 사이에서의 역할과 협업에 대해 배울 수 있었다. 소통을 하면서 서로 자신의 코드를 설명하고, 각자 맡은 파트에서 아는 지식을 공유하면서, 몰랐던 백엔드의 지식을 얻을 수 있을 때가 정말 너무너무 재밌었다.🥺👍
.env 파일을 다루는게 쉽지 않았지만 모르던 개념도 많이 알게 되었고, 성공적으로 통신이 되어서 뿌듯하다. :)!
1차 때처럼 학원을 오가던 길에서 틈틈히 Oauth 2.0에 대해 공부했는데 신기하고 재미있는 지식을 많이 접할 수 있어서 좋았다. 이 부분은 다시 공부해서 정리하고 싶다!
2차에서는 백엔드 팀원이 API명세서를 초반에 잘 작성해 주셔서 이부분을 미리 참고하며 작업할 수 있었던 점이 좋았던 것 같다. 😀
소셜로그인 파트를 해보길 너무 잘했다!✨
마지막 날에는 다른 파트를 담당해서 아직 해보지 못했던 우리 팀원과 몇몇 동기분들에게 이 정보를 공유해 드리고 싶어서 우리팀 백엔드분과 함께 내가 이해한 로직을 설명해 드리는 스터디 모임도 가졌다. 😆👍
슬비님 블로그 알았으면 프로젝트 끝나고 바로 댓글 달았을텐데 늦었지만 남기고 가요 👍👍👍