PJH's Community Site - Community(1)

박정호·2022년 11월 23일
0

Community Project

목록 보기
6/14
post-thumbnail

🚀 Start

이제 이 프로젝트의 핵심인 커뮤니티 기능을 추가해보자.
커뮤니티가 생성된 페이지를 만들기 앞서 커뮤니티 생성 페이지를 만들어보자!



🖥 Client

✔️ Create page 생성

커뮤니티을 생성하는 페이지가 필요할 것이다. 따라서, subs라는 폴더 안에 create.tsx를 생성할 것다.

이때 sub는 커뮤니티이라는 의미로 사용할 것이다.(sub = subject)



✔️ UI 작성

form 태그 안에는 input태그button태그가 존재.

  • onSubmit으로 handleSubmit이라는 메서드 호출.

input태그를 재사용할 수 있도록 InputGroup 컴포넌트import

  • props로 필요한 값들을 전달
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;


✔️ Create 기능 추가

State 생성

  • 다음과 같은 state들을 api요청에 담길 데이터들
  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);
    }
  };


📡 Server

지금까지 본 것은 client에서 입력한 커뮤니티에 대해서 server로 데이터를 post하고 요청하는 것이었다. 따라서, 그에 맞는 api를 생성하여 response를 보내주자.



✔️ 커뮤니티 생성을 위한 api 생성

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);


✔️ createSub 함수 작성

const createSub = async (req: Request, res: Response) => {
  const { name, title, description } = req.body;
	...
};
  
  
const router = Router();

router.post('/',createSub);

export default router;


✔️ user, auth 미들웨어 생성

client에서도 user에 대한 정보를 많은 컴포넌트에서 필요로 하기 때문에 Context에 담아서 데이터 관리를 해주었다.

Server에서도 마찬가지로 user에 대한 정보를 많은 핸들러가 필요로 하고, 유저정보 또는 등급에 따라 인증을 해주는 기능 도한 자주 필요하므로 재사용성을 위해서 미들웨어로 분리해주자.

  • User Middleware : 여러 핸들러에서 유저 정보를 필요
  • Auth Middleware : 유저 정보 또는 유저 등급에 따른 인증이 필요

파일 생성

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)



  • 실제로 useMiddleware를 통해서 유저 정보를 잘 불러오는지를 확인해보자!

💡 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 : datasource
  • getRepository: 모든 엔티티에 대한 일반 저장소 (참고)
  • 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️⃣ 인스턴스 생성

  • name, description, title 생성

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 등록

  • me 핸들러 실행 전에 user,auth 미들웨어를 거치면서 토큰(쿠키) 유무를 확인
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')이 되면서 authenticatedtrue가 되는 action이 동작하게 된다. 따라서, true라면 즉, 로그인이 되었다는 뜻으로 로그인,회원가입 페이지에서는 메인페이지로 이동하게 된다.

login.tsx / register.tsx - client

import { useAuthState } from '../context/auth';
...
const { authenticated } = useAuthState();
...
const router = useRouter();
...
if (authenticated) router.push('/');


📷 Photos

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글

관련 채용 정보