이제 이 프로젝트의 핵심인 커뮤니티 기능을 추가해보자.
커뮤니티가 생성된 페이지를 만들기 앞서 커뮤니티 생성 페이지를 만들어보자!
커뮤니티을 생성하는 페이지가 필요할 것이다. 따라서, subs라는 폴더 안에 create.tsx를 생성할 것다.
이때 sub는 커뮤니티이라는 의미로 사용할 것이다.(sub = subject)
✅ form 태그
안에는 input태그
와 button태그
가 존재.
✅ input태그
를 재사용할 수 있도록 InputGroup 컴포넌트
를 import
const SubCreate = () => {
...
return (
<div>
<div>
<h1>커뮤니티 만들기</h1>
<hr/>
<form onSubmit={handleSubmit}>
<div>
<p>Name</p>
<p>
커뮤니티 이름은 변경할 수 없습니다.
</p>
<InputGroup
placeholder="이름"
value={name}
setValue={setName}
error={errors.name}
/>
</div>
<div>
<p>Title</p>
<p>
주제를 나타냅니다. 언제든지 변경할 수 있습니다.
</p>
<InputGroup
placeholder="제목"
value={title}
setValue={setTitle}
error={errors.title}
/>
</div>
<div>
<p>Description</p>
<p>
해당 커뮤니티에 대한 설명입니다.
</p>
<InputGroup
placeholder="설명"
value={description}
setValue={setDescription}
error={errors.description}
/>
</div>
<div>
<button>
커뮤니티 만들기
</button>
</div>
</form>
</div>
</div>
);
};
export default SubCreate;
✅ State 생성
const [name, setName] = useState('');
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [errors, setErrors] = useState<any>({});
✅ form태그의 submit으로 실행되는 handleSubmit 생성
1️⃣ 백엔드에 로그인을 위한 요청 (name, title, description을 post)
3️⃣ error시 setErrors를 통해 reponse값 저장.(에러에 대한 문구)
const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
try {
const res = await axios.post('/subs', { name, title, description });
router.push(`/r/${res.data.name}`);
} catch (error: any) {
console.log(error);
setErrors(error.response.data);
}
};
지금까지 본 것은 client
에서 입력한 커뮤니티에 대해서 server
로 데이터를 post하고 요청하는 것이었다. 따라서, 그에 맞는 api를 생성하여 response를 보내주자.
✅ routes폴더 안에 subs.ts 생성
src/routes/subs.ts
const createSub = async (req: Request, res: Response) => {
const { name, title, description } = req.body;
};
const router = Router();
router.post('/', createSub);
export default router;
✅ Entry파일에 sub route import
app.get('/', (_, res) => res.send('running'));
app.use('/api/auth', authRoutes);
app.use('/api/subs', subRoutes);
const createSub = async (req: Request, res: Response) => {
const { name, title, description } = req.body;
...
};
const router = Router();
router.post('/',createSub);
export default router;
client에서도 user에 대한 정보를 많은 컴포넌트에서 필요로 하기 때문에 Context에 담아서 데이터 관리를 해주었다.
Server에서도 마찬가지로 user에 대한 정보를 많은 핸들러가 필요로 하고, 유저정보 또는 등급에 따라 인증을 해주는 기능 도한 자주 필요하므로 재사용성을 위해서 미들웨어로 분리해주자.
✅ 파일 생성
✅ User Middleware 작성
1️⃣ 요청의 쿠키에 담겨있는 토큰을 가져오기
2️⃣ verify
메서드와 jwt secret
을 이용해서 토큰을 디코딩
3️⃣ 토큰에서 나온 유저 이름을 이용해서 유저 정보를 데이터베이스에서 가져오기
4️⃣ 유저 정보를 res.locals.user
에 넣어주기
export default async (req: Request, res: Response, next: NextFunction) => {
try {
const token = req.cookies.token; // 1️⃣ 번
if (!token) return next();
const { username }: any = jwt.verify(token, process.env.JWT_SECRET); // 2️⃣ 번
const user = await User.findOneBy({ username }); // 3️⃣ 번
res.locals.user = user; // 4️⃣ 번
} catch (error) {
console.log(error);
return res.status(400).json({ error: 'Something went wrong' });
}
};
✅ Auth Middleware 작성
export default async (req: Request, res: Response, next: NextFunction) => {
try {
const user: User | undefined = res.locals.user;
if (!user) throw new Error('Unauthenticated');
return next();
} catch (error) {
console.log(error);
return res.status(401).json({ error: 'Unauthenticated' });
}
};
✅ createSub 핸들러에 미들웨어 적용
const createSub = async (req: Request, res: Response, next) => {
const { name, title, description } = req.body;
};
const router = Router();
router.post('/', userMiddleware, authMiddleware, createSub);
export default router;
💡 잠깐) Node.js Middleware?(res, req, next)
💡 TypeError: Cannot read properties of undefined (reading 'token')
위와 같은 에러가 발생하는 이유는 token이 없다는 것으로 즉, 유저정보에 따라 게시글을 게시할 수 있는데 인증이 되지 않은 것이다.
왜냐하면 client에서 데이터 요청을 할 때 token 전달이 되지 않았기 때문이다.
const res = await axios.post('/subs', { name, title, description });
1️⃣ 표준 CORS요청은 기본적으로 쿠키를 설정하거나 보낼 수 없다.
2️⃣ 프론트에서 axios요청할 때, withCredentials부분을 true로 해서 수동으로 CORS 요청에 쿠키값을 넣어줘야 한다.
3️⃣ 마찬가지로 서버도 응답헤더에 Access-Control-Allow-Credentials를 true로 설정해야 한다.
✅ 따라서, Credentials 부분을 true로 설정한다.
const res = await axios.post( '/subs', { name, title, description }, { withCredentials: true } );
하지만, 앞서 인증 페이지를 구현할 때도 withCredentials를 true로 설정해줘야 했는데, 모든 axios에 한꺼번에 true로 설정하면 좋을 것 같다.
✅ 따라서, 상위 파일인 설정해놓자
// _app.tsx Axios.defaults.baseURL = process.env.NEXT_PUBLIC_SERVER_BASE_URL + '/api'; Axios.defaults.withCredentials = true;
✅ 마지막으로 백엔드에서는 cookie-parser를 설정하자.
cookie-parser
: 요청된 쿠키를 쉽게 추출할 수 있도록 도와주는 미들웨어설치
npm install cookie-parser --save npm i --save-dev @types/cookie-parser
적용
// server.ts app.use(express.json()); app.use(morgan('dev')); app.use(cookieParser());
✅ 이제 커뮤니티 생성시 토큰이 전달되는 것을 확인할 수 있다.
만약 로그인이 되어 있다면 위와 같이 정상적으로 token이 전달되는 것을 확인할 수 있다.
그럼 이제 확인해야 할 것은 게시글 작성시 이름과 제목을 입력했는지를 확인해야하며, 동일 게시글이 존재하지는 않는지를 검사해보자.
✅ 존재할 경우
1️⃣ 이름과 제목은 반드시 작성되야하므로 isEmpty
를 통해 판별.
2️⃣ 이미 동일한 커뮤니티가 존재하는지 확인.
AppDataSource
: datasourcegetRepository
: 모든 엔티티에 대한 일반 저장소 (참고)createQueryBuilder
: SQL Query를 생성하고 실행한 다음 자동적으로 변형된Entity를 반환.where
: name이 존재하는지 찾는다.getOne
: 같은 이름은 하나만 있을 수 있는 조건.💡 참고하자 👉 Select using Query
3️⃣ 커뮤니티가 존재한다면 경고글 출력
4️⃣ 만약 error가 존재한다면, 즉 유효성검사에 걸린다면 throw문 실행
throw
: 사용자 정의 예외를 발생(throw)할 수 있다. 예외가 발생하면 현재 함수의 실행이 중지되고 throw 이후의 명령문은 실행되지 않는다.try {
const errors: any = {};
if (isEmpty(name)) errors.name = '이름을 비워둘 수 없습니다.'; // 1️⃣ 번
if (isEmpty(title)) errors.title = '제목을 비워둘 수 없습니다.'; // 1️⃣ 번
const sub = await AppDataSource.getRepository(Sub) // 2️⃣ 번
.createQueryBuilder('sub')
.where('lower(sub.name) = :name', { name: name.toLowerCase() })
.getOne();
if (sub) errors.name = '이미 게시글이 존재합니다.'; // 3️⃣ 번
if (Object.keys(errors).length > 0) { // 4️⃣ 번
throw errors;
}
} catch (error) {
return res.status(500).json({ error: "문제가 발생했습니다." });
}
✅ 존재하지 않을 경우
1️⃣ res.locals를 사용
res.locals
: 렌더링시 중복되는 값들을 저장해놓고 계속해서 사용 가능2️⃣ 인스턴스 생성
3️⃣ 쿠키가 있다면 그 쿠키를 이용해서 백엔드에서 인증 처리하기
4️⃣ client에 전달
try {
const user: User = res.locals.user; // 1️⃣ 번
const sub = new Sub(); // 2️⃣ 번
sub.name = name;
sub.description = description;
sub.title = title;
sub.user = user;
await sub.save(); // 3️⃣ 번
return res.json(sub); // 4️⃣ 번
} catch (error) {
console.log(error);
return res.status(500).json({ error: "문제가 발생했습니다." });
}
이제 로그인이 된 사람만 커뮤니티 생성이 가능하게 만들어보자.
따라서, 로그인이 되지 않은 경우 커뮤니티 생성 페이지에 접속하지 못하게 하면 될 것이다.
1️⃣ getServerSideProps 사용
getServerSideProps
: getServerSideProps요청 시 데이터를 가져와야 하는 페이지를 렌더링해야 하는 경우에만 사용해야 한다. 이는 요청의 데이터 또는 속성(예: authorization헤더 또는 지리적 위치)
의 특성 때문일 수 있다. 사용하는 페이지 getServerSideProps는 요청 시 서버 측에서 렌더링되며 캐시 제어 헤더가 구성된 경우에만 캐시된다.💡 참고하자 👉 getServerSideProps
2️⃣ 쿠키가 없다면 에러를 보내기
3️⃣ 커뮤니티 존재한다면 경고글 출력
4️⃣ 백엔드에서 요청에서 던져준 쿠키를 이용해 인증 처리할 때 에러가 나면 /login
페이지로 이동
client - create.tsx
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
try {
const cookie = req.headers.cookie; // 1️⃣ 번
if (!cookie) throw new Error('Missing auth token cookie'); // 2️⃣ 번
// 3️⃣ 번
await axios.get(`/auth/me`, {
headers: { cookie },
});
return { props: {} };
} catch (error) { // 4️⃣ 번
res.writeHead(307, { Location: '/login' }).end();
return { props: {} };
}
};
✅ auth에 router 등록
server - auth.ts
const me = async (_: Request, res: Response) => {
return res.json(res.locals.user);
};
const router = Router();
router.get('/me', userMiddleware, authMiddleware, me);
router.post('/register', register);
router.post('/login', login);
✅ 쿠키를 지우고 /subs/create
페이지에 들어가면 로그인 페이지로 이동하는 것을 확인 가능!
마지막으로 해야할 것은 로그인에 따른 페이지 이동이다. 만약 로그인이 되었는데 로그인 또는 회원가입 url을 입력한다면 해당 페이지로 가지 않고 로그인이 되었으니 메인페이지로 이동하게 만들자.
✅ context/auth.tsx - client
useEffect(() => {
async function loadUser() {
try {
const res = await axios.get('/auth/me');
dispatch('LOGIN', res.data);
} catch (error) {
console.log(error);
} finally {
dispatch('STOP_LOADING');
}
}
loadUser();
}, []);
useEffect
로 로그인시에 dispatch('LOGIN')
이 되면서 authenticated
가 true
가 되는 action
이 동작하게 된다. 따라서, true
라면 즉, 로그인이 되었다는 뜻으로 로그인,회원가입 페이지에서는 메인페이지로 이동하게 된다.
✅ login.tsx / register.tsx - client
import { useAuthState } from '../context/auth';
...
const { authenticated } = useAuthState();
...
const router = useRouter();
...
if (authenticated) router.push('/');