How to become a real programmer/Back-End

Kotlin + Ktor + React 프로젝트를 Java + Spring + React 프로젝트로(2 - MVC 패턴 먹이고 게시판 가져오자)

MinDDokDDok 2025. 1. 13. 10:20

Spring Boot 기반의 프로젝트는 MVC 패턴을 따르면 코드의 구조화 및 유지보수성을 향상시킬 수 있다고 말을 하며 이전 포스팅을 마쳤다. 이번에도 해당 패턴을 적용할 것이다.

아 근데 좀 말투가 너무 딱딱한가요? 아까 쓴 글을 보니 뭔가 너무 딱딱한 사람 같아서 뭐 네 그냥 존댓말로 하도록 하겠습니다.

우선 이미지 처럼, FinanceNewsProvider 라는 이름의 프로젝트 아래에 Controller, Entity, Model, Repository, Service 패키지를 만들어 줍니다

 

각 패키지들이 MVC 패턴에서 무슨 역할을 하느냐를 간단히 짚어보겠습니다

controller Controller 클라이언트의 HTTP 요청을 처리하고, 적절한 서비스 호출 및 응답 반환.
entity Model (Entity) 데이터베이스 테이블과 매핑되는 클래스 정의 (ORM 엔티티).
model Model (DTO/VO) 데이터 전달 객체(Data Transfer Object) 또는 값 객체(Value Object).
repository Data Access Layer 데이터베이스와의 상호작용을 담당하며, CRUD 기능 제공.
service Service 비즈니스 로직을 처리하며, 컨트롤러와 리포지토리 사이에서 중간 역할 수행.

 

위처럼, 저 또한 이번 프로젝트를 생성하면서 Entity와 Model 두개가 있을 필요가 있나? 라는 생각이었는데 claude는 이렇게 두 개로 나누어서 Entity 패키지에서는 DB 테이블과 매핑되는 클래스를 정의하고, Model 패키지에서는 데이터 전달 객체 혹은 값 객체를 정의한다고 이야기 했습니다.

 

구현순서는 가장 쉬운 파트부터 하겠습니다.

기존 프로젝트는 주식 정보 + 주식 관련 뉴스정보 + 커뮤니티 기능이므로, 커뮤니티로 CRUD를 체험하고, 주식정보 불러오는 기능을 구현한 후 뉴스정보를 가져오는게 순서가 괜찮겠죠?

 

흠 그럼 기본 골조를 잡아볼까요 순서를 정하겠습니다.

 

1. Entity (Post.java) 정의하기

  • 데이터베이스 테이블과 매핑되는 Java 객체
  • JPA 엔티티로 정의되어 데이터베이스 테이블 구조를 표현
  • Lombok 어노테이션을 통해 반복적인 코드(getter, setter 등) 자동 생성
package com.example.FinanceNewsProvider.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data               // getter, setter, toString 등을 자동으로 생성
@Entity             // JPA 엔티티임을 나타냄
@Table(name = "posts") // 실제 DB 테이블 이름 지정
@Builder            // 빌더 패턴 구현
@NoArgsConstructor  // 매개변수 없는 생성자
@AllArgsConstructor // 모든 필드를 매개변수로 받는 생성자
public class Post {
    @Id // 기본키 지정
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본키 생성 전략 설정
    private Integer id;

    @Column(length=255, nullable=false) // 칼럼 속성 정의
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    @Column(length=255, nullable = false)
    private String author;

    @Column(name="created_at")
    private LocalDateTime createdAt;
}

 

2. Repository (PostRepository.java) 정의하기

  • 데이터베이스 작업을 담당하는 인터페이스
  • JpaRepository를 상속받아 기본적인 CRUD 작업 메서드 제공
  • 필요한 경우 커스텀 쿼리 메서드 추가 가능
package com.example.FinanceNewsProvider.repository;


import com.example.FinanceNewsProvider.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Post, Integer> {
    // JpaRepository에서 기본적인 CRUD 메서드를 제공합니다 개꿀입니다
    // findAll(), findById(), save(), deleteById() 등
}

 

3. Service (PostService.java) 정의하기

  • 비즈니스 로직을 처리하는 계층
  • Repository를 통해 데이터베이스 작업 수행
  • @Transactional 어노테이션으로 트랜잭션 관리
  • 데이터 검증, 변환 등의 작업 수행
package com.example.FinanceNewsProvider.service;

import com.example.FinanceNewsProvider.entity.Post;
import com.example.FinanceNewsProvider.repository.PostRepository;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
public class PostService {
    private final PostRepository postRepository;

    @Transactional
    public Post createdPost(Post post){
        post.setCreatedAt(LocalDateTime.now());
        return postRepository.save(post);
    }

    @Transactional(readOnly=true)
    public List<Post> getAllPosts(){
        return postRepository.findAll();
    }

    @Transactional(readOnly=true)
    public Post getPostById(Integer id){
        return postRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Post not found"));
    }

    @Transactional
    public Post updatePost(Integer id, Post postDetails){
        Post post = getPostById(id);
        post.setTitle(postDetails.getTitle());
        post.setContent(postDetails.getContent());
        post.setCreatedAt(LocalDateTime.now());
        return postRepository.save(post);
    }

    @Transactional
    public void deletePost(Integer id){
        postRepository.deleteById(id);
    }

}

 

4. Controller (PostController.java) 정의하기

  • 클라이언트의 HTTP 요청을 처리하는 계층
  • URL 매핑을 통해 적절한 엔드포인트 제공
  • 요청 데이터를 검증하고 Service 계층으로 전달
  • 응답 데이터를 클라이언트에게 반환
package com.example.FinanceNewsProvider.controller;

import com.example.FinanceNewsProvider.entity.Post;
import com.example.FinanceNewsProvider.service.PostService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController                 // REST API 컨트롤러임을 나타냄 --> 차후 글 작성
@RequestMapping("/api/posts")   // 기본 URL 경로 설정
@RequiredArgsConstructor        // final 필드에 대한 생성자 자동 생성
@CrossOrigin(origins = "http://localhost:3000") // React 앱과의 CORS 설정
public class PostController {
    private final PostService postService;


    // 게시글 생성 기능
    @PostMapping
    public ResponseEntity<Post> createpost(@RequestBody Post post) {
        Post createdPost = postService.createdPost(post);
        return new ResponseEntity<>(createdPost, HttpStatus.CREATED);
    }

    // 모든 게시글 조회 기능
    @GetMapping
    public ResponseEntity<List<Post>> getAllPosts() {
        List<Post> posts = postService.getAllPosts();
        return ResponseEntity.ok(posts);
    }

    // 특정 게시글 조회 기능
    @GetMapping("/{id}")
    public ResponseEntity<Post> getPostById(@PathVariable Integer id){
        Post post = postService.getPostById(id);
        return ResponseEntity.ok(post);
    }

    // 게시글 수정 기능
    @PutMapping("/{id}")
    public ResponseEntity<Post> updatePost(@PathVariable Integer id,
                                           @RequestBody Post post) {
        Post updatedPost = postService.updatePost(id, post);
        return ResponseEntity.ok(updatedPost);
    }

    // 게시글 삭제 기능
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deletePost(@PathVariable Integer id) {
        postService.deletePost(id);
        return ResponseEntity.noContent().build();
    }
}

 

이렇게 네 가지 파일이 준비되면 기본적인 골조는 완성된겁니다. CRUD 기능을 위해 백엔드에서 필요한 model, repository, service, controller을 생성했으니 이제 세부 설정을 해야겠죠

application.properties에 몇가지 정보를 추가해주면 됩니다.

spring.application.name=FinanceNewsProvider

# 서버 포트를 설정합니다 = 서버가 실행될 포트 번호를 8081로 설정
server.port = 8081 

# MariaDB 정보를 설정합니다
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
# MariaDB 데이터베이스 드라이버 클래스 이름
spring.datasource.url = jdbc:mariadb://localhost:3306/vacation_ktor
# MariaDB 데이터베이스 URL (localhost의 3306 포트를 사용, 데이터베이스 이름은 vacation_ktor)
spring.datasource.username = m1
# MariaDB에 접속할 사용자 이름
spring.datasource.password = 123456
# MariaDB에 접속할 사용자 비밀번호

# JPA 설정을 합니다 --> 차후 글 작성

spring.jpa.hibernate.ddl-auto=validate
# Hibernate가 데이터베이스 스키마를 자동으로 관리하는 방식. 'validate'는 기존 스키마를 검증만 하고 수정하지 않음
spring.jpa.show-sql=true
# JPA 실행 시 SQL 쿼리를 콘솔에 출력하도록 설정
spring.jpa.properties.hibernate.format_sql=true
# 출력되는 SQL 쿼리를 보기 좋게 포맷팅해서 출력
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
# Hibernate에 사용될 MariaDB의 SQL 방언(dialect)을 설정

# Jackson JSON 설정 (날짜 형식) --> 차후 글 작성
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
# JSON 날짜를 출력할 형식 설정 (예: 2023-04-01 14:30:00)
spring.jackson.time-zone=Asia/Seoul
# JSON 날짜에 사용할 시간대 설정 (Asia/Seoul로 설정하여 대구 사람이지만 서울 시간대로 출력)

 

기본설정들을 위 처럼 마쳐주면, 이제 CRUD 기능을 사용할 수 있을까요? 우선 그 전에 build.gradle 파일에서 필요한 dependencies 들이 추가되었는지 확인해야합니다. 저는 아래처럼 되어 있네요.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.1'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
       languageVersion = JavaLanguageVersion.of(21)
    }
}

configurations {
    compileOnly {
       extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mariadb.jdbc:mariadb-java-client:3.1.4'

    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

}

tasks.named('test') {
    useJUnitPlatform()
}

 

자 이제 그럼 실행 될까요? 아쉽게도 아직 프론트를 만지지 않았죠. 글 작성이고 뭐고 다 해당 form이 있어야 백엔드가 처리할 수 있으므로 만들어야 합니다. 그냥 이대로 실행하고 제 port인 http://localhost:8081로 들어간다면?

기본적으로 Spring Security는 모든 엔드포인트를 보호하려고 합니다

 

이런 화면이 나타나는데, 설정을 피해가기 위해 간단히 설정을 해줄까요.

resources-static에 간단한 index.html을 우선 생성해주고(Spring Security 에러 우회 확인용),

<!DOCTYPE html>
<html>
<head>
    <title>Finance News Provider</title>
    <link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<h1>Welcome to Finance News Provider</h1>
<div id="root"></div>
<script src="/js/main.js"></script>
</body>
</html>

 

build.gradle에 dependecies를 추가해줘야 합니다

implementation 'org.springframework.boot:spring-boot-starter-security'

 

그리고 프로젝트 루트(com.example.프로젝트명)에서 config 폴더를 하나 만들고 거기 SecurityConfig.java를 만들어 줍니다

// src/main/java/com/example/FinanceNewsProvider/config/SecurityConfig.java
package com.example.FinanceNewsProvider.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())  // CSRF 보호 비활성화
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/**").permitAll()  // 모든 요청 허용
                );

        return http.build();
    }
}

 

이렇게 하고 실행해보면 

아까와는 달리 모든 권한을 허용하고, 로그인 페이지가 나타나지 않으며 DB로의 접근 또한 기존에 설정한 username, password로 접근해서 잘 보여집니다

 

 

자 이제 front를 꾸미기 위해 React 프로젝트를 생성해주죠

기존 루트에서 아래 코드를 작성해주시면 되는데요.

npx create-react-app frontend
cd frontend
npm install axios @mui/material @emotion/react @emotion/styled @mui/icons-material

 

각 line에 대해 설명하자면

    1. npx create-react-app frontend: React 애플리케이션을 frontend 디렉토리에 생성합니다.
    2. cd frontend: frontend 디렉토리로 이동합니다.
    3. npm install axios @mui/material @emotion/react @emotion/styled @mui/icons-material: React 애플리케이션에 axios와 Material UI 관련 라이브러리를 설치합니다.

확인해볼까요, ㅋㅋ

React 기본 설정이 완료된 것을 확인할 수 있습니다

 

Front의 각 구조는 아래처럼 생성할 생각입니다.

저는 Java + SpringBoot 기반의 프로젝트 경험이 거의 없어서 여기서 Kotlin과의 차이점이구나! 착각하고 알게된 부분이 있습니다.

Kotlin의 frontEnd는 main - Resources에 생성하는게 일반적이어서 그렇게 개발하고 있었는데

Java + Spring + React 프로젝트를 개발하는데 frontEnd를 그냥 프로젝트 폴더에 생성하는 예제가 많았습니다. 물론 Java + Spring + React도 resources에 frontEnd 를 구현해도 되지만 

 

 
  • Ktor + React:
    • 서버 중심 애플리케이션이나 소규모 프로젝트에 적합.
    • 배포 간소화: 서버와 프론트를 함께 빌드하고 배포할 수 있어 편리.
  • Spring + React:
    • 대규모 프로젝트나 팀 개발에서 많이 사용.
    • 프론트와 백엔드를 완전히 독립적으로 관리하고 개발할 수 있음.
    • React 개발 서버를 별도로 활용하면 Hot Reloading  API Proxy 설정이 쉬워 프론트엔드 개발이 편

이렇게 규모와 효율에 따라서 다르게 프로젝트 틀을 고민해 결정하는것이 좋겠구나 생각했습니다 물론 정해진건 없습니다. 아무튼 각설하고 넘어가쥬.

 

지금 당장 사용하지 않는 파일들은 좀 삭제해주고(App.test.js - 단위테스트 시 사용하는 파일, setupTests.js - 테스트 환경설정 파일, reportWebVitals.js - 웹 성능측정 파일) src 폴더에 components 폴더를 생성했습니다. 그리고 내부에 Post 정보를 가져와 띄워줄 PostForm.jsPostList.js를 생성했습니다.

 

<PostForm.js> : PostList에서 글 작성을 위해 버튼을 누르면 나오게 되는 모달, 정보를 기입해서 DB로 보낸다

import React, { useState } from 'react';  // React와 useState 훅 import
// Material-UI 컴포넌트들 import
import {
    Button,         // 버튼
    TextField,      // 입력 필드
    Dialog,         // 모달 창
    DialogTitle,    // 모달 제목
    DialogContent,  // 모달 내용
    DialogActions,  // 모달 하단 버튼 영역
    Box            // 레이아웃 컴포넌트
} from '@mui/material';

// props로 받는 값들:
// open: 모달 창 열림/닫힘 상태
// handleClose: 모달 닫기 함수
// handleSubmit: 폼 제출 처리 함수
// initialData: 수정 시 기존 게시글 데이터
const PostForm = ({ open, handleClose, handleSubmit, initialData }) => {
    // useState로 폼 데이터 상태 관리
    // initialData가 있으면(수정 시) 그 값을 사용, 없으면(새글 작성 시) 빈 값으로 초기화
    const [post, setPost] = useState(initialData || {
        title: '',
        content: '',
        author: ''
    });

    // 폼 제출 처리 함수
    const onSubmit = (e) => {
        e.preventDefault();         // 기본 폼 제출 동작 방지
        handleSubmit(post);        // 상위 컴포넌트의 제출 함수 호출
        handleClose();             // 모달 창 닫기
    };

    return (
        // Dialog: 모달 창 컴포넌트
        <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
            <DialogTitle>
                {initialData ? '게시글 수정' : '새 게시글 작성'}
            </DialogTitle>
            <DialogContent>
                {/* 폼 컴포넌트 */}
                <Box component="form" onSubmit={onSubmit} sx={{ mt: 2 }}>
                    {/* 제목 입력 필드 */}
                    <TextField
                        fullWidth
                        label="제목"
                        margin="normal"
                        value={post.title}
                        onChange={(e) => setPost({...post, title: e.target.value})}
                        required
                    />
                    {/* 내용 입력 필드 */}
                    <TextField
                        fullWidth
                        label="내용"
                        margin="normal"
                        multiline         // 여러 줄 입력 가능
                        rows={4}          // 4줄 높이
                        value={post.content}
                        onChange={(e) => setPost({...post, content: e.target.value})}
                        required
                    />
                    {/* 작성자 입력 필드 */}
                    <TextField
                        fullWidth
                        label="작성자"
                        margin="normal"
                        value={post.author}
                        onChange={(e) => setPost({...post, author: e.target.value})}
                        required
                    />
                </Box>
            </DialogContent>
            {/* 버튼 영역 */}
            <DialogActions>
                <Button onClick={handleClose}>취소</Button>
                <Button onClick={onSubmit} variant="contained" color="primary">
                    {initialData ? '수정하기' : '작성하기'}
                </Button>
            </DialogActions>
        </Dialog>
    );
};

export default PostForm;

 

<PostList.js> : 글 작성 목록을 확인, 삭제 및 수정을 할 수 있다

import React, { useState, useEffect } from 'react';  // React와 훅들 import
// Material-UI 컴포넌트들 import
import {
    Box,
    Button,
    Paper,           // 카드 같은 elevation이 있는 컨테이너
    List,            // 목록 컴포넌트
    ListItem,        // 목록 항목
    ListItemText,    // 목록 항목 텍스트
    Typography,      // 텍스트 스타일링
    IconButton,      // 아이콘 버튼
} from '@mui/material';
// Material-UI 아이콘들 import
import { Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
import axios from 'axios';          // HTTP 요청 라이브러리
import PostForm from './PostForm';  // 게시글 작성/수정 폼

const PostList = () => {
    // 상태 관리
    const [posts, setPosts] = useState([]);              // 게시글 목록
    const [openForm, setOpenForm] = useState(false);     // 폼 모달 열림/닫힘
    const [selectedPost, setSelectedPost] = useState(null); // 선택된 게시글(수정 시)

    // 컴포넌트 마운트 시 게시글 목록 가져오기
    useEffect(() => {
        fetchPosts();
    }, []);

    // 게시글 목록 가져오기 함수
    const fetchPosts = async () => {
        try {
            const response = await axios.get('/api/posts');
            setPosts(response.data);
        } catch (error) {
            console.error('Failed to fetch posts:', error);
        }
    };

    // 게시글 저장/수정 처리 함수
    const handleSubmit = async (postData) => {
        try {
            if (selectedPost) {  // 수정
                await axios.put(`/api/posts/${selectedPost.id}`, postData);
            } else {            // 새글 작성
                await axios.post('/api/posts', postData);
            }
            fetchPosts();       // 목록 새로고침
            setSelectedPost(null);  // 선택된 게시글 초기화
        } catch (error) {
            console.error('Failed to save post:', error);
        }
    };

    // 게시글 삭제 처리 함수
    const handleDelete = async (id) => {
        try {
            await axios.delete(`/api/posts/${id}`);
            fetchPosts();  // 목록 새로고침
        } catch (error) {
            console.error('Failed to delete post:', error);
        }
    };

    return (
        <Box>
            {/* 새 게시글 작성 버튼 */}
            <Button
                variant="contained"
                color="primary"
                onClick={() => setOpenForm(true)}
                sx={{ mb: 2 }}
            >
                새 게시글 작성
            </Button>

            {/* 게시글 목록 */}
            <Paper elevation={3}>
                <List>
                    {/* 각 게시글을 순회하며 표시 */}
                    {posts.map((post) => (
                        <ListItem
                            key={post.id}
                            divider
                            secondaryAction={  // 우측에 표시될 버튼들
                                <Box>
                                    {/* 수정 버튼 */}
                                    <IconButton onClick={() => {
                                        setSelectedPost(post);
                                        setOpenForm(true);
                                    }}>
                                        <EditIcon />
                                    </IconButton>
                                    {/* 삭제 버튼 */}
                                    <IconButton onClick={() => handleDelete(post.id)}>
                                        <DeleteIcon />
                                    </IconButton>
                                </Box>
                            }
                        >
                            {/* 게시글 내용 */}
                            <ListItemText
                                primary={post.title}  // 제목
                                secondary={           // 내용과 메타 정보
                                    <>
                                        <Typography component="span" variant="body2">
                                            {post.content}
                                        </Typography>
                                        <br />
                                        <Typography component="span" variant="caption">
                                            작성자: {post.author} | 작성일: {new Date(post.createdAt).toLocaleString()}
                                        </Typography>
                                    </>
                                }
                            />
                        </ListItem>
                    ))}
                </List>
            </Paper>

            {/* 게시글 작성/수정 폼 모달 */}
            <PostForm
                open={openForm}
                handleClose={() => {
                    setOpenForm(false);
                    setSelectedPost(null);
                }}
                handleSubmit={handleSubmit}
                initialData={selectedPost}
            />
        </Box>
    );
};

export default PostList;

 

이렇게 해줍니다.

그리고 기존에 있던 App.js와 index.js를 요로코롬 바꿔주면

 

<App.js>

// 메인 컴포넌트 파일 App.js
import React from 'react';
// Material-UI 테마 관련 import
import { ThemeProvider, createTheme } from '@mui/material/styles';
// Material-UI 컴포넌트들 import
import {
    Container,
    CssBaseline,    // CSS 초기화
    AppBar,         // 상단 네비게이션 바
    Toolbar,        // AppBar 내부 컨텐츠 영역
    Typography,     // 텍스트 표시 컴포넌트
    Box            // div와 비슷한 레이아웃 컴포넌트
} from '@mui/material';
import PostList from './components/PostList';  // 게시글 목록 컴포넌트

// Material-UI 기본 테마 생성
const theme = createTheme();

function App() {
    return (
        // ThemeProvider로 테마 적용
        <ThemeProvider theme={theme}>
            {/* CssBaseline: CSS 초기화 */}
            <CssBaseline />
            {/* Box: 전체 레이아웃을 감싸는 컨테이너 */}
            <Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
                {/* AppBar: 상단 네비게이션 바 */}
                <AppBar position="static">
                    <Toolbar>
                        {/* Typography: 텍스트 스타일링 */}
                        <Typography variant="h6">
                            게시판
                        </Typography>
                    </Toolbar>
                </AppBar>
                {/* Container: 내용을 중앙에 배치하는 컨테이너 */}
                <Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
                    <PostList />  {/* 게시글 목록 표시 */}
                </Container>
            </Box>
        </ThemeProvider>
    );
}

export default App;  // 다른 파일에서 import할 수 있도록 내보내기

 

<index. js>

import React from 'react';                      // React 라이브러리 import
import { createRoot } from 'react-dom/client';  // React 18의 새로운 렌더링 방식
import './index.css';                          // 전역 스타일
import App from './App';                       // 최상위 컴포넌트

// DOM에서 'root' id를 가진 요소를 찾아 React 앱을 렌더링
const root = createRoot(document.getElementById('root'));

// React.StrictMode는 개발 모드에서 잠재적인 문제를 찾아주는 도구
root.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

 

기본 설정은 끝난겁니다.

그리고 package.json에 가서 proxy 설정을 해주어야 하는데요. 저는 spring 포트가 8081을 사용하고 있기에

package.json 가장 아래 단의 괄호 닫히기 전

"proxy": "http://localhost:8081"

 

을 추가해줬습니다. 이렇게 하고 spring 서버를 킨 후 terminal로 frontEnd로 가 npm start를 해주게 되면

 

기존에 있던 DB를 사용했던 터라, Post에 속했던 글 정보들이 불러와졌습니다

 

이렇게 잘 나타나는 것을 확인할 수 있습니다.

이제 주식정보 검색 & 뉴스정보 매칭 기능이 남았네요. 다음 글에서 계속 하겠습니다.