DB와 웹페이지 간 데이터 전달 + 웹페이지에 정보 정렬, 검색 기능 구현

Angela·2022년 11월 14일
0

이 글은 React, Node.js, mongoDB를 연결하여 서로 데이터를 주고 받는 작업에 대해 전반적으로 설명하고, 이를 바탕으로 검색 기능을 구현하는 것을 목적으로 하고 있습니다. 또한 JSX, ANT-Design CSS 프레임워크를 이용하였음을 미리 적어둡니다.

저는 이번 프로젝트에서 프론트엔드/백엔드를 맡았습니다.
이 플젝을 구현하면서 가장 중요하게 생각한 부분은
1. 검색 시스템
2. 추천 판례 연결
이라고 할 수 있었는데요.
그중에서도 '검색 시스템'의 경우 DB에 정보를 넣고, 그 정보를 다시 가져오고, DB에서 원하는 정보를 찾아야하는 작업입니다.
그래서 이 글에는 그런 검색시스템을 구현하기 위해 필요한 코드와 과정을 설명하고자 합니다.

웹페이지에서 작성한 정보를 DB로 전송

node.js로 구현한 서버와 react로 구현한 클라이언트가 잘 연결되어있다는 가정 하에 클라이언트의 업로드 페이지에서 입력한 정보가 어떻게 DB로 저장되는지에 관한 route를 설명하겠습니다.

아래의 사진은 Client 부분의 프로그램 구조입니다.

위 사진에서 Client/src/components/views/UploadCasePage가 보이실텐데요.
그 안의 UploadCasePage.js의 일부 코드를 아래에 적어놓았습니다.
이 코드에서 유념할 부분은 onChange Event를 처리하는 것입니다.
onChange Event를 처리하는 이유는 웹페이지의 입력창에 입력한 정보를 value로 받아 DB에 넘기기 위함입니다. 즉, 사용자의 입력에 따라 달라지는 dynamic value를 처리하기 위해서라고 생각하면 됩니다.

function UploadPage(props) {

  // 여기는 value의 State를 정의한 것입니다. 
  // initial state는 ""로 되어있는 것(empty string)을 확인할 수 있습니다.
  
  const [TitleValue, setTitleValue] = useState("")
  const [DescriptionValue, setDescriptionValue] = useState("")
  
  
  // 여기는 이벤트를 처리하기 위해 value를 가져오는 직업을 하는 부분입니다.
  // 타이핑을 할 때마다 value를 바꾸기 위해 사용하는 부분인 것입니다.
  // 이 코드가 있어야 빈 칸에 제대로 타이핑이 가능합니다.
  
  const onTitleChange = (event) => {
      setTitleValue(event.currentTarget.value)
  }

  const onDescriptionChange = (event) => {
      setDescriptionValue(event.currentTarget.value)
  }

// 여기까지 코드를 이해했으면 남은 코드는 잠시 미뤄두고 아래의 >모델 정의<로 넘어갑시다.



// 업로드 페이지에 정보를 모두 입력했으면 이를 DB에 보내는 작업이 남았는데요.
// 입력창에 입력한 정보가 보내지도록 이를 request하는 handler 코드입니다.

 const submitHandler = (event) => {
        event.preventDefault(); //웹페이지가 자동적으로 refresh되는 것을 막습니다.

		//모든 State가 채워져야만 이벤트를 처리할 수 있도록 하였습니다.
        if (!TitleValue || !DescriptionValue ) {
            return alert('모든 값을 넣어주셔야 합니다.')
        }

        //서버의 모델 정의와 웹페이지의 value를 연결시켜주는 코드
        
        const body = {    
            title: TitleValue,
            description: DescriptionValue,
        }

		// 서버로 보내기 위한 Route 설정
        // server/routes/case.js에서 정의한 end point인 /api/case로 연결됩니다.
        // props.history.push('/')는 업로드 성공시 '/'의 주소로 이동하는 것입니다. 
        // 여기서는 LandinPage를 '/'로 지정했기에 LandinPage로 이동합니다.
        
        Axios.post("/api/case", body) 
            .then(response => {
                if (response.data.success) {
                    alert('성공적으로 업로드 되었습니다.')
                    props.history.push('/')
                } else {
                    alert('업로드를 실패하였습니다.')
                }
            })
} // 여기까지 이해했으면 아래의 서버 route 설정으로 내려갑시다.


// 아래의 return은 각각의 입력창과 확인 버튼을 구현한 것입니다.
// 입력창에 입력시 onChange Event가 발생하게 하였으며, 
// 확인 버튼을 누를 시 Submit Event가 발생합니다. 

  return (
    <div style={{ maxWidth: '700px', margin: '2rem auto' }}>

      <Form onSubmit={submitHandler}>
        {/* 이 Form에서 받은 input은 submitHandler를 통헤 DB로 보내집니다. */}
        
        <label>제목</label>
        <Input
            onChange={onTitleChange} 
            value={TitleValue}   
        />
        
        <label>설명</label>
        <Input
            onChange={onDescriptionChange}
            value={DescriptionValue}
         />

        <Button onClick={submitHandler}>
            확인
        </Button>

      </Form>
    </div>
  )
}

export default UploadPage

웹페이지의 입력창에 타이핑까지는 가능해졌는데 이걸 DB에 보내기 위해서는 서버를 거쳐야 합니다. 그리고 서버에서 DB로 이를 보내주어야 하죠.
그런데 서버에서 이 정보를 그냥 보내면 안될 것입니다.
그러기 위해서는 서버에서 정보를 어떻게 보내는지 모델을 정의해야합니다.

이 사진은 sever의 구조인데요. 사진을 보면 models 폴더가 보이실 겁니다.
이 폴더 안에 case.js에 모델 정의(Schema)에 관한 코드가 있습니다.
Schema 안에 필드를 정의하는 것입니다.

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const caseSchema = mongoose.Schema({
 
    title: {
        type: String
    },
    description: {
        type: String
    }    
})

const Case = mongoose.model('Case', caseSchema);
module.exports = { Case }

이렇게 서버에서 DB로 보내기 위한 Schema 정의도 끝났습니다.
다시 위의 UploadPage.js로 가봅시다.

여기서는 서버의 route가 담긴 server/routes/case.js 코드 일부를 보려합니다.

const express = require('express');
const router = express.Router();
const { Case } = require("../models/Case"); //모델 형식을 가져옵니다.

// UploadPage.js에서 axios의 /api/case의 경로는 아래로 연결됩니다.

router.post("/", (req, res) => { 

    // Client로부터 얻은 모든 data를 DB에 저장합니다.
    const case1 = new Case(req.body)
    case1.save((err) => {
        if (err) return res.status(400).json({ success: false, err })
        return res.status(200).json({ success: true })
    })
}); // 에러가 날 때와 성공했을 때를 구분할 수 있게 하였습니다.

그렇다면 이제 웹페이지에서 보낸 정보가 DB에 잘 저장되는지를 확인해보겠습니다.
아래는 제가 구현한 웹페이지의 업로드 페이지입니다.

성공적으로 업로드 되었다는 메세지와 DB에 저장된 모습을 볼 수 있습니다.

DB에 있는 데이터를 웹페이지에 불러오기

LandingPage에 DB에 넣은 데이터들을 불러와보겠습니다.

아래는 LandingPage.js 코드의 일부입니다.
위에서 봤던 코드와 비슷한 것이 보일텐데요.
여기서는 서버의 /api/case/cases의 경로로 연결하고 있는 것을 확인할 수 있습니다.

import React, { useEffect } from 'react'
import axios from "axios";

function LandingPage() {
    
    useEffect(() => {
        axios.post('/api/case/cases')
            .then(response =>{
                if (response.data.success){
                	console.log(response.data) //확인용 코드
                }
                else{
                    alert("판례를 가져오는데 실패하였습니다.")
                }
            })
    },[])

    return (
        <div>
           
        </div>         
    )
}

export default LandingPage

위에서 end point가 /api/case/cases인 서버의 route 경로가 나왔기 때문에 server/routes/case.js 코드 일부를 보려합니다.
/cases로 end point를 일치시켜주었습니다.
CaseInfo는 모든 정보가 담긴 곳입니다.

DB에 저장된 정보를 찾는 코드이고, 가져오는 걸 성공하는지 실패하는지에 따라 다르게 하였습니다. 이 정보들이 정말 잘 가져와지는지를 확인해보겠습니다.

router.post("/cases", (req, res) => { 

        Case.find()
        .exec((err, caseInfo) => {
            if (err) return res.status(400).json({success:false, err})
            return res.status(200).json({
                success:true, caseInfo
            })
        })    
});

위에서 성공하면 LandingPage.js 안의 console.log(response.data)를 실행하기 때문에 크롬의 개발자도구를 틀면 확인할 수 있습니다.
아까 넣은 정보가 정상적으로 불러와졌습니다.

DB에서 불러온 정보를 바탕으로 한 검색 기능 구현

이렇게 검색창에 test를 입력하면 아까 넣은 제목이 test인 정보를 검색할 수 있게 검색 기능에 관한 코드를 설명하겠습니다.

먼저 LandingPage의 코드를 수정하기 전에 SearchFeature.js를 /LandingPage/Section 폴더 안에 만들어주겠습니다.

아래는 SearchFeature.js 코드입니다.

import React, { useState } from 'react'
import { Input } from 'antd';
const { Search } = Input;

function SearchFeature(props) {
    
    // 검색어(value) 저장을 위한 state 선언, initial state는 빈 칸 
    const [SearchTerms, setSearchTerms] = useState("")

	// 검색 이벤트 발생 시 이벤트 처리를 위한 onChange Function
    // 검색어를 다르게 입력할 때마다 value에 그 값을 넣어주는 역할.
    
    const searchHandler = (event) => {
        setSearchTerms(event.currentTarget.value)
        props.refreshFunction(event.currentTarget.value) 
    }

// props.refreshFunction은 계속 바뀌는 value를 LandingPage.js와 연결시켜줍니다. 

// 실제로 검색하려면 client 화면에 나타나야 하기 때문에 아래에 코드 구현
  return (
    <div>
       <Search
            placeholder="검색어를 입력해주세요."
            onChange={searchHandler}
            value={SearchTerms}    
        /> 
    </div>
  )
}

export default SearchFeature

위의 SearchFeature.js로 검색기능을 위해 구현해야하는 state와 onChange Function은 모두 구현하였습니다.
그리고 props.refreshFunction을 통해 LandingPage로의 연결도 해놓았습니다.
그러나 LandingPage에서 SearchFeature.js의 부분을 받는 코드를 구현하지 않았기 때문에 이 부분을 구현해야합니다.

일단 SearchFeature.js를 LandingPage에서 쓸 수 있게 import 할 것입니다. LandingPage.js 코드의 일부입니다.

import SearchFeature from './Sections/SearchFeature'; 
// SearchFeature.js를 LandingPage.js에 import합니다.

const [SearchTerms, setSearchTerms] = useState("")

// SerchFeature.js의 코드들은 실질적으로 LandingPage에서 실행되어야 하기 때문에 
// newSearchTerm을 받아줄 부모 컴포넌트가 필요합니다.
// 그를 위해 updateSearchTerm을 만들어 사용하기로 합니다.
// 또한 검색어를 창에 단순히 입력하는 것이 아닌 검색어에 맞는 케이스를
// DB에서 가져오는 것이 최종 목표이므로 그에 해당하는 코드 또한 있어야 합니다.

const updateSearchTerms = (newSearchTerm) => {

     const body = {
  
         searchTerm: newSearchTerm
     } 

     setSearchTerms(newSearchTerm)
     getCases(body) // 검색어(searchTerm)에 맞는 데이터를 DB에서 가져옵니다. 
}


// 아래의 return <div></div> 안에 아래의 {/* Search */} 코드를 넣습니다.
// SearchFeature.js에서 props.refreshFunction의 부분이 아래와 연결됩니다.

	{/* Search  */}
            <div>
                <SearchFeature
                     refreshFunction={updateSearchTerms}
                />
            </div>
    

LandingPage.js에서 검색을 할 수 있게 구현했지만 실제로 검색어에 맞는 데이터를 DB에서 찾는 작업으로 이어져야 합니다.
이는 server/routes/case.js에서 검색을 위한 코드가 추가되어야함을 의미합니다. (서버가 클라이언트를 DB와 연결시켜주기 때문)
즉, getCases로 DB에서 정보를 가져올 때 추가되는 부분이 있어야 합니다.

case.js의 코드를 보겠습니다.
위에 있던 case.js의 코드와 다른 점이 있다면 위에서 나왔던 부분은 else{}안에 있고, 지금은 검색어에 따라 검색을 해주기 위해 if (term){} 부분이 추가되었다는 것입니다.


router.post("/cases", (req, res) => { 
   
    let term = req.body.searchTerm 

    if (term){
        Case.find()
        .find({ "_" : { $regex: term } }) //여기 인덱스를 뭐로 바꾸냐에 따라 검색하는게 달라짐
        //.find({ $text: { $search: term }})
        .exec((err, caseInfo) => {
            if (err) return res.status(400).json({success:false, err})

            return res.status(200).json({
                success:true, caseInfo
            })
        })

    } else {
        Case.find()
        .exec((err, caseInfo) => {
            if (err) return res.status(400).json({success:false, err}) 

            return res.status(200).json({
                success:true, caseInfo
            })
        })
    }
});

그럼 다른 부분을 좀 더 자세히 봐보겠습니다.

if (term) {
        Case.find()
        .find({ "title" : { $regex: term } }) 
        //.find({ $text: { $search: term }})
        .exec((err, caseInfo) => {
            if (err) return res.status(400).json({success:false, err})

            return res.status(200).json({
                success:true, caseInfo
            })
        })
 }

.find({ "title" : { $regex: term } })
//.find({ $text: { $search: term }})
이 부분이 궁금하실텐데요.

regex의 경우 우리가 흔히 생각하는 Like 검색을 구현한 것이라고 생각하시면 됩니다. 여기서는 title이라는 항목에서 사용자가 검색하는 검색어가 존재하는지를 like 검색하는 것이라고 볼 수 있습니다.
사용자가 '계란'을 검색했다하면 title이 계란찜, 계란후라이 인 데이터들이 결과로 나오겠지요. 이것이 Like 검색입니다.

그럼 아래의 //.find({ $text: { $search: term }})은 무엇일까요?
이것은 사용하지 않아서 주석처리 해두었지만 정확히 검색하는 것이 들어맞아야 검색이 되는 경우에 사용하는 코드입니다.
즉, '계란찜'을 찾고 싶다면 이 코드를 사용할 경우 사용자는 검색어로 '계란찜'을 정확히 입력해야합니다.

그 외에도 Search 기능이 더욱더 궁금하시다면
regex 메뉴얼 / text 메뉴얼을 참고하시면 좋을 듯 합니다.

이렇게 저희는 DB에 데이터를 넣고 다시 가져오고, DB에 있는 데이터들을 검색하는 것까지 구현해보았습니다.
이러한 기능들을 이용한 코드 전체는 깃허브에 있습니다. 감사합니다.

0개의 댓글