이번에 블로깅하는 검색 페이지 설정 파트가 제일 까다로웠던 부분이였다. 라이브러리들을 사용했지만 이미지 업로드, 드래그 앤 드롭을 통한 순서 변경, 각 섹션의 JSON 타입의 데이터 추가/삭제 등 한 페이지에 구현해야 할 기능들이 많아서 머릿속으로 생각했던 코드들이 정상적으로 작동할지 걱정이 되었다. 비동기 요청과 관련된 부분에서 예상치 못한 버그가 발생했지만 다행히 해결해서 기능 구현을 마칠 수 있었다.
//Login.js
import React, { useEffect, useRef } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserPlus } from '@fortawesome/free-solid-svg-icons';
import { GoogleLogin } from 'react-google-login';
import axios from 'axios';
import './Login.css';
export default function Login({ login, loginModal }) {
const naverRef = useRef();
const initializeNaverLogin = () => {
const naverScript = document.createElement('script');
naverScript.src =
'https://static.nid.naver.com/js/naveridlogin_js_sdk_2.0.2.js';
naverScript.type = 'text/javascript';
document.head.appendChild(naverScript);
naverScript.onload = () => {
const naverLogin = new window.naver.LoginWithNaverId({
clientId: process.env.REACT_APP_NAVER_CLIENT_ID,
callbackUrl: process.env.REACT_APP_REDIRECT_URI,
isPopup: false,
loginButton: { color: 'white', type: 3, height: '47' },
});
naverLogin.init();
naverLogin.logout();
};
};
const kakaoLoginHandler = () => {
const { Kakao } = window;
Kakao.Auth.login({
scope: '',
success: () => {
Kakao.API.request({
url: '/v2/user/me',
success: async function (res) {
axios
.post(
`${process.env.REACT_APP_SERVER_API}/users/kakao-login`,
{
identification: res.id,
},
{ withCredentials: true }
)
.then(() => {
window.location.replace('/');
});
},
});
},
});
};
const onSuccess = async (response) => {
const { googleId } = response;
axios
.post(
`${process.env.REACT_APP_SERVER_API}/users/google-login`,
{
identification: googleId,
},
{ withCredentials: true }
)
.then(() => {
window.location.replace('/');
});
};
const onFailure = (error) => {
console.log(error);
};
const guestLogin = () => {
axios
.post(`${process.env.REACT_APP_SERVER_API}/users/guest-login`, '', {
withCredentials: true,
})
.then((res) => {
window.location.replace('/');
})
.catch((err) => {
console.log(err);
});
};
useEffect(() => {
initializeNaverLogin();
}, []);
const handleClick = () => {
naverRef.current.children[0].click();
};
return (
<div className={loginModal ? 'login-container' : 'hidden'}>
<div className='box-login' ref={login}>
<div className='box-login-btn'>
<div className='btn-naver'>
<div ref={naverRef} id='naverIdLogin' />
</div>
<img
src='img/btn-login-naver.png'
alt='naver-login'
onClick={handleClick}
className='btn-naver'
/>
<img
src='img/btn-login-kakao.png'
alt='naver-kakao'
className='btn-kakao'
onClick={kakaoLoginHandler}
/>
<GoogleLogin
clientId={process.env.REACT_APP_GOOGLE_CLIENT_ID}
render={(renderProps) => (
<img
src='img/btn-login-google.png'
alt='naver-google'
className='btn-google'
onClick={renderProps.onClick}
/>
)}
responseType={'id_token'}
onSuccess={onSuccess}
onFailure={onFailure}
/>
<div className='btn-guest' onClick={guestLogin}>
<FontAwesomeIcon icon={faUserPlus} className='icon-user' />
<span className='text-guest'>게스트 로그인</span>
</div>
</div>
</div>
</div>
);
}
//게스트 로그인 응답
const { User } = require('../../models');
const jwt = require('jsonwebtoken')
module.exports = async (req, res) => {
try {
let duplication = true;
let randomSet = '';
while (duplication) {
randomSet = Math.random().toString(36).slice(2, 15);
const check = await User.findOne({
where : { identification: randomSet }
})
duplication = !!check;
console.log(duplication);
}
const user = await User.create({
identification : randomSet,
oauth: 'guest',
siteName: 'FAKESEARCH',
themeColor: '#2260FF'
})
const accessToken = jwt.sign(
user.dataValues,
process.env.ACCESS_SECRET,
{ expiresIn: "7d" }
);
res
.cookie("accessToken", accessToken)
.status(201)
.end();
} catch (err) {
...
};
게스트 로그인과 소셜 로그인(네이버, 카카오) 모두 이전 프로젝트에서 구현한 적이 있기 때문에 어렵지 않게 구현할 수 있었고, 구글은 처음이였지만 리액트에서 사용할 수 있는 모듈이 있어서 쉽게 구현할 수 있었다. 게스트 로그인의 경우 서버에서 랜덤으로 문자열을 생성해서 기존에 있는 유저인지 확인 과정을 거쳐 유저 정보를 생성하고 로그아웃 시 데이터를 삭제하는 방식으로 구현하였다.
메인 페이지에서 state에 따라서 로그인 모달 창이 보이도록 {modalLogin&&<Login/>}
이런 형태로 코드를 작성했으나 위 gif 이미지 처럼 렌더링되는데 시간이 걸리는 버그가 있어서 className이 변경될때 CSS의 display:none
설정이 적용되도록 변경하여 해결하였다.
//이미지 업로드 요청 const onDrop = async (pictureFiles) => { const body = new FormData(); body.append('files', pictureFiles[0]); const res = await axios.post( `${process.env.REACT_APP_SERVER_API}/post/upload_files`, body, { headers: { 'Content-Type': 'multipart/form-data' }, } ); dispatch( changeProfile({ profileImg: `${process.env.REACT_APP_SERVER_API}/${res.data.filename}`, }) ); };
//index.js(서버) ... var storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, "public/"); }, filename: function (req, file, cb) { let ext = file.originalname.split("."); ext = ext[ext.length - 1]; cb(null, `${Date.now()}.${ext}`); }, }); ... const upload = multer({ storage: storage }); app.use([express.static("public"), upload.array("files")]); ...
//이미지 업로드 응답 module.exports = (req, res) => { try { console.log(req.files) if (req.files.length > 0) { res.json(req.files[0]); } } catch (err) { ... } };
모든 섹션(프로필, 뉴스, 이미지, 음악)에서 이미지를 업로드 할 수 있는 기능이 필요해서 react-images-upload
라는 라이브러리를 사용해서 이미지가 전달될 수 있도록 컴포넌트를 따로 생성해주었다. 해당 라이브러리의 기능은 이미지의 확장자와 최대 크기를 제한해주고 미리보기 기능을 제공해주는 것인데, 미리보기는 원하는 형태로 구현이 어려워서 별도로 만들어 주었다.
react-images-upload
는 클라이언트에서 파일을 인자로 전달해주는 기능까지만 제공하기 때문에 서버에서 이미지를 저장하고 클라이언트에 변경된 파일명을 다시 전달해주는 부분은 따로 설정해주었다.
맨 처음에 진행한 다가치 프로젝트에서 ckeditor와 multer를 사용하여 이미지 없로드를 구현한 적이 있기 때문에 참고하여 코드를 작성하였다. 파일의 최대 크기를 제한하는 부분은 라이브러리에서 해주고 있기 때문에 따로 설정하지 않았고, 파일의 이름이 중복되지 않도록 날짜를 사용해서 변경하고 public 폴더에 이미지를 저장하고 파일명을 다시 클라이언트에 전달해주는 부분을 따로 설정해주었다.
react-beautiful-dnd
를 사용해서 드래그 기능을 구현했는데 섹션의 순서가 다른 단어로 검색어를 변경할때마다 map으로 보여지는 섹션의 컴포넌트들이 무한히 생성되는 버그가 발생하였다.
처음에는 왜 발생하는 버그인지 파악이 되지 않았으나 각 섹션의 리듀서가 분리되어 있다보니 order에 따라서 sort 될때 섹션별로 데이터가 바뀌기 때문에 이전에 데이터가 남은 채로 map이 실행되서 발생하는 버그라고 판단했다.
리듀서를 하나로 통일하면 해결되겠지만 이미 각 섹션의 데이터 수정, 추가, 삭제 기능이 구현된 이후였고 4개의 리듀서를 합치면 데이터 관리가 어렵고 추후 섹션을 추가할 때도 번거롭기 때문에 다른 방법이 필요했다.
생각보다 해결법이 간단했는데 resetDrag
라는 state를 통해 검색어가 바뀔 때마다 데이터를 가져오기 전에 드래그하는 부분이 사라지고 ({!resetDrag && (섹션 설정 관련 부분)}
) 데이터를 받아오면 resetDrag이 false가 되면서 다시 보여지는 방식으로 해결하였다.
단어가 바뀔때마다 섹션들이 깜박이면서 변하기 때문에 유저 입장에서도 단어에 따라서 섹션의 데이터가 변경되었는지 알기 쉽게 되었다.
...
export default function SearchData({ themeColor }) {
const [resetDrag, setResetDrag] = useState(false);
...
const selectSearchData = async (e) => {
setSelected(e);
setResetDrag(true);
setOpenProfile(false);
setOpenNews(false);
setOpenImage(false);
setOpenMusic(false);
const res = await axios.get(
`${process.env.REACT_APP_SERVER_API}/search/word`,
{
params: { word: e.value },
withCredentials: true,
}
);
dispatch(changeProfile(res.data.profile));
dispatch(changeNews(res.data.news));
dispatch(changeImage(res.data.image));
dispatch(changeMusic(res.data.music));
setResetDrag(false);
};
...
return (
<div className='searchdata-container'>
...
<div className='box-section'>
...
<div className='setting-section'>
...
{!resetDrag && (각 섹션의 설정 컴포넌트가 보여지는 부분)}
...
</div>
);
}
가장 까다로운 부분의 기능 구현이 끝나고 실제 검색 페이지만 구현하면 되기 때문에 앞으로 언제 프로젝트를 마칠 수 있을지 예상할 수 있었다. 이번 파트에서 자바스크립트가 어떻게 동작하는지 생각하면서 로직을 짜야된다고 느낄 수 있었다. 비동기적 처리를 제대로 이해할 수 있도록 관련된 공부를 깊게 해야되겠다는 생각이 들었다.