목업 페이지를 완성하고 검색 페이지 설정을 제외한 다른 설정의 기능 구현을 먼저 시작하였다. 유저 정보가 데이터베이스에 저장되는 부분까지 한번에 구현하는 것이 편할 것이라고 생각해서 Sequelize의 migration이나 회원가입과 관련된 API 처럼 서버에 필요한 부분을 먼저 구현하게 되었다.
//migrations/AutoCompletes
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('AutoCompletes', {
...
userId: {
type: Sequelize.INTEGER,
onDelete: 'CASCADE',
references:{model: 'Users', key: 'id'}
},
...
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('AutoCompletes');
}
};
//models/AutoCompletes
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class AutoComplete extends Model {
...
static associate(models) {
AutoComplete.belongsTo(models.User,{
foreignKey: "userId",
onDelete: "CASCADE",
})
...
};
이전 프로젝트에서 Sequelize 때문에 많은 에러(https://github.com/codestates/Shall-We-Health/projects/3)와 마주 했는데 이번 프로젝트에서 사용한 이유는 모든 에러의 해결법을 찾았었고 필요한 쿼리문을 정리해보니 Raw Query 없이 Sequelize 메소드만으로 모든 기능을 구현할 수 있다고 판단해서 였다. 다만, Sequelize는 백엔드 팀원 분께서 담당하셨기 때문에 CASCADE
관련 설정을 할때 약간 헤맸지만 models와 migrations 모두 onDelete: 'CASCADE'
설정이 필요하다는 것을 알고 변경해주었다.
게스트 로그아웃과 회원탈퇴 시 Users.id를 외래키로 가지고 있는 테이블의 데이터들도 자동으로 삭제되어야 하기 때문에 외래키 설정과 함께 cascade 설정도 함께 해주었다.
//네이버 로그인 요청
const jwt = require("jsonwebtoken");
const { User } = require("../../models/index");
const axios = require("axios");
module.exports = async (req, res) => {
const { token } = req.body;
if (!token) {
return res.status(400).json({
data: null,
error: {
path: "users/naver",
message: "Insufficient body data",
},
});
}
const userData = await axios.get("https://openapi.naver.com/v1/nid/me", {
headers: {
Authorization: `Bearer ${token}`,
},
});
const identification = userData.data.response.id;
try {
const [user, created] = await User.findOrCreate({
where: { identification },
defaults: {
oauth: 'naver',
siteName: 'FAKESEARCH',
themeColor:'#2260FF',
},
});
if (!created) {
/* 회원가입이 되어있을 때 */
const accessToken = jwt.sign(
user.dataValues,
process.env.ACCESS_SECRET,
{ expiresIn: "7d" }
);
res
.cookie("accessToken", accessToken)
.status(200)
.end();
} else {
/* 회원가입 되어 있지 않을 때 */
const accessToken = jwt.sign(
user.dataValues,
process.env.ACCESS_SECRET,
{ expiresIn: "7d" }
);
res
.cookie("accessToken", accessToken)
.status(201)
.end();
}
} catch (err) {
...
};
설정 기능을 구현하기 위해서는 유저의 데이터가 저장되어 있어야 하기 때문에 네이버 로그인부터 구현하였다. 클라이언트에서 네이버에 요청해서 받은 token을 서버에 전달해주면 서버에서 네이버에 유저정보를 받아와서 findOrCreate
메소드로 유저정보를 조회하거나 새로 삽입해서 accessToken을 Cookie에 담아서 응답하는 방식을 사용하였다.
이전 프로젝트에서 카카오와 네이버 OAuth 로그인을 구현하였기 때문에 빠르게 진행할 수 있었던 것 같다. naver 로그인은 보안 상의 이유로 서버 환경에서만 유저정보 요청이 가능하지만 카카오와 구글은 클라이언트에서 바로 유저 정보를 전달해주기 때문에 중간의 axois 요청을 제외한 동일한 로직으로 구현할 예정이다.
//checkSiteName.js
export default function checkSiteName(siteName) {
const name = siteName
const checkForm = new RegExp(/^[가-힣a-zA-Z]{2,10}$/);
const checkDomain = ['naver','daum','kakao','google','nate','네이버','다음','카카오','구글','네이트']
if(!checkForm.test(name) || name === null || checkDomain.some((el) => name.toLowerCase().includes(el))) {
return false
} else return true
}
사이트 이름을 변경하는 기능은 간단한 로직이지만 사이트 이름을 검사하는 함수를 따로 만들어 주었다. 한글, 영어로만 구성된 2~10글자여야 되고 혹시나 악용될 가능성 때문에 유명 포털 사이트의 이름이 포함되지 않은 단어로만 가능하도록 설정하였다.
//사이트 이름 및 테마 색상 설정
import React, { useState, useEffect, useRef } from 'react';
import Color from './Color';
...
export default function Site() {
const { themeColor, siteName } = useSelector((state) => state.loginReducer)
const btnColor = useRef()
const boxColor = useRef()
const [modalColor, setModalColor] = useState(false)
...
const handleClickOutside = ({ target }) => {
if (!boxColor.current.contains(target) && !btnColor.current.contains(target)) setModalColor(false);
};
const handleCheck = (e) => {
setNicknameForm(e.target.value)
if(e.target.value==='') {
setIsChecked(true)
} else {
setIsChecked(checkSiteName(e.target.value))
}
}
...
useEffect(() => {
window.addEventListener("click", handleClickOutside);
return () => {
window.removeEventListener("click", handleClickOutside);
};
}, []);
return (
<div className='site-container'>
...
<div className='box-theme'>
<div className='title-theme'>테마 색상</div>
<div className='info-color' ref={btnColor}>
<div id='sample-color' style={{backgroundColor: themeColor}} onClick={()=>{setModalColor(!modalColor)}}/>
<div id='text-color' onClick={()=>{setModalColor(!modalColor)}}>{themeColor}</div>
</div>
<Color boxColor={boxColor} modalColor={modalColor} themeColor={themeColor}/>
</div>
</div>
)
}
테마 색상 적용 부분은 react-color 라이브러리를 사용하였고 useRef를 사용하여 색상이나 색상 코드를 눌렀을때 설정 창이 나오고 외부를 클릭했을때 닫히도록 설정해주었다.
//Color.js
...
export default function Color({boxColor, modalColor }) {
const dispatch = useDispatch();
const { themeColor } = useSelector((state) => state.loginReducer)
const hadleThemeColor = (e) => {
axios.patch(`${process.env.REACT_APP_SERVER_API}/users/theme-color`, {
themeColor : e.hex
},{withCredentials: true})
.then (()=>{
dispatch(login({themeColor : e.hex}))
})
}
return <div ref={boxColor} className={modalColor ? 'color-container' : 'hidden'}>
<ChromePicker color={themeColor} onChange={hadleThemeColor}/>
</div>;
}
위 코드는 색상이 변경 되었을때 서버에 색상 코드를 전달하고 받아서 리듀서에도 값을 변경해주는 부분인데 처음에 매뉴얼대로 themeColor : e
이런식으로 적어주었으나 에러가 발생했다. console.log(e)
로 확인해보니 e
자체가 색상코드 값이 아니라 여러 값을 가진 객체여서 필요한 헥스코드를 전달하도록 변경하였다.
//Main.js
import React, { useState, useEffect, useRef } from 'react';
import filterAutoComplete from '../utils/filterAutoComplete';
...
export default function Main() {
...
const [searchWord, setSearchWord] = useState('');
const [autoComplete, setAutoComplete] = useState([]);
const [focus, setFocus] = useState(false);
const [modal, setModal] = useState(false);
...
const handleSeachWord = async (e) => {
setSearchWord(e.target.value);
if (
filterAutoComplete(e.target.value) !== '' &&
e.target.value.replace(/(\s*)/g, '') !== '' &&
isLogin
) {
const word = filterAutoComplete(e.target.value);
const res = await axios.get(
`${process.env.REACT_APP_SERVER_API}/auto/filtered`,
{ params: { word }, withCredentials: true }
);
setAutoComplete(res.data);
}
if (filterAutoComplete(e.target.value).replace(/(\s*)/g, '') === '') {
setAutoComplete([]);
}
};
return (
<div className='main-container'>
...
<div className='searchForm-container'>
...
<input
type='text'
className='search'
onChange={handleSeachWord}
onFocus={() => {
setFocus(true);
}}
onBlur={() => {
setFocus(false);
}}
onKeyPress={(e) => {
if (e.key === 'Enter') {
window.location.replace(`/search/query=${searchWord}`);
}
}}
></input>
...
{!(searchWord === '' && autoComplete.length === 0) && focus &&
autoComplete.map((el, id) => {
return (
<AutoList
key={id}
el={el}
searchWord={searchWord}
themeColor={themeColor}
></AutoList>
);
})}
...
</div>
</div>
);
}
//filterAutoComplete.js
export default function filterAutoComplete(autoWord) {
return autoWord.replace(/[^가-힣a-zA-Z0-9~!@#$%^&*()_+|<>?:{};\-=`.,/ ]+/g,'')
}
자동 완성 검색어를 추가하고 삭제하는 기능은 간단했지만 실제로 검색 창에서 보여지는 부분이 예상치 못한 버그가 있어서 조건을 추가해주었다. 자동완성 검색어가 가, 가나, 가나다
이렇게 세개의 단어가 있다면 가ㄴ
처럼 자음만 입력한 상태여도 자동 완성 검색어는 가, 가나, 가나다
모두 표시가 되어야하고 input 창에 포커스를 해제하면 자동완성 단어 리스트가 보이지 않도록 구현하기 위해서 filterAutoComplete 함수와 여러 state를 만들어서 조건을 걸어주었다.
이번 프로젝트에서는 React를 좀 더 깊게 이해하고 에러를 핸들링하며 기능들에 필요한 로직을 생각해내는 과정이 의미가 있다고 생각한다. 새로운 스킬을 배우는 것도 좋지만, 기존에 사용했던 스택에 대해서 깊게 이해하는 과정이 필요하다. 다양한 스택을 경험하고 능숙하게 사용할 수 있으면 좋겠지만, 신입 개발자로써 기초를 튼튼하게 다져놓는 것이 지금은 더 중요한 것 같다.