1. Vue 에 DB 정보 띄우기
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
// 프론트에서 /api로 시작하는 요청을 보내면 백엔드로 전달함
'/api': {
target: 'http://localhost:9090/naver', // 내 백엔드 주소 (Context Path 포함)
changeOrigin: true,
secure: false,
}
}
}
})
src/App.vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
// 1. 데이터 타입 정의 (백엔드 UserDto와 맞춤)
interface User {
userId: string;
userName: string;
email: string;
}
// 2. 반응형 변수 생성
const userData = ref<User | null>(null)
const loading = ref(true)
// 3. 페이지가 로딩될 때 백엔드 호출
onMounted(async () => {
try {
// /api/user/naver_test 라고 호출하면 Proxy가 알아서 http://localhost:9090/naver/api/user/naver_test 로 보내줌
const response = await axios.get('/api/user/naver_test')
userData.value = response.data
loading.value = false
} catch (error) {
console.error('데이터를 가져오지 못했습니다:', error)
loading.value = false
}
})
</script>
<template>
<div class="container">
<header>
<h1 class="logo">NAVER <span>Clone</span></h1>
</header>
<main v-if="!loading">
<div v-if="userData" class="profile-card">
<h3>내 정보 (From Oracle DB)</h3>
<p><strong>아이디:</strong> {{ userData.userId }}</p>
<p><strong>이름:</strong> {{ userData.userName }}</p>
<p><strong>이메일:</strong> {{ userData.email }}</p>
</div>
<p v-else>사용자 정보를 찾을 수 없습니다.</p>
</main>
<p v-else>데이터 불러오는 중...</p>
</div>
</template>
<style scoped>
.container { font-family: sans-serif; padding: 20px; }
.logo { color: #03cf5d; font-weight: bold; }
.logo span { color: #000; }
.profile-card {
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
max-width: 300px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
</style>
터미널
npm install axios
다시 VITE를 시작하는 방법
npm run dev

근데 사용자 정보를 찾을 수 없다고 뜬다.
백엔드단 처리를 안 해주어 그렇다.
다시 Springboot 프로젝트로 이동
com.naver.clone.dto.UserDto.java
package com.naver.clone.dto;
import lombok.Data; // 이게 있으면 Getter; Setter를 자동으로 생성
@Data
public class UserDto {
private String userId;
private String userName;
private String email;
}
com.naver.clone.mapper.UserMapper.java
package com.naver.clone.mapper;
import com.naver.clone.dto.UserDto;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
// XML의 <select id="getUserById"> 와 이름이 동일해야 합니다.
UserDto getUserById(String id);
}
src/main/resources/mapper/UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.naver.clone.mapper.UserMapper">
<select id="getUserById" resultType="com.naver.clone.dto.UserDto">
SELECT USER_ID, USER_NAME, EMAIL
FROM USERS
WHERE USER_ID = #{id}
</select>
</mapper>
여기서 의문이
실제 DB 컬럼값과 UserDto의 변수값이 다른데, 그땐 설정을 해 주어야 한다.
+ Mapper 위치도 지정해 주어야 한다.
src/main/resources/application.properties
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.mapper-locations=classpath:mapper/*.xml

이제 DB 값이 잘 조회가 된다.
2. 네이버 UI/UX 레이아웃 작업
UI/UX 부분은 도저히 모르겠어서 AI에게 맡겼다
App.vue
<template>
<div class="naver-clone">
<header class="header">
<div class="header-inner">
<h1 class="logo">NAVER</h1>
<div class="search-area">
<input type="text" class="search-input" placeholder="검색어를 입력해 주세요." />
<button class="search-btn">🔍</button>
</div>
</div>
</header>
<nav class="gnb">
<ul class="menu-list">
<li>메일</li><li>카페</li><li>블로그</li><li>쇼핑</li><li>뉴스</li>
</ul>
</nav>
<main class="content">
<section class="left-section">
<div class="news-stand">뉴스스탠드 영역 (준비 중)</div>
</section>
<aside class="right-section">
<div v-if="user" class="login-box">
<div class="user-info">
<span class="user-name"><strong>{{ user.userName }}</strong>님 환영합니다!</span>
<span class="user-email">{{ user.email }}</span>
</div>
<button class="logout-btn">로그아웃</button>
</div>
</aside> </main>
</div>
</template>
<style scoped>
.header { background: #fff; border-bottom: 1px solid #e4e7e8; padding: 20px 0; }
.header-inner { max-width: 1080px; margin: 0 auto; display: flex; align-items: center; }
.logo { color: var(--naver-green); font-size: 30px; margin-right: 30px; }
.search-area {
display: flex;
border: 2px solid var(--naver-green);
width: 500px;
border-radius: 2px;
}
.search-input { border: none; padding: 10px; flex: 1; outline: none; }
.search-btn { background: var(--naver-green); border: none; color: white; padding: 0 15px; cursor: pointer; }
.gnb { background: #fff; border-bottom: 1px solid #e4e7e8; }
.menu-list {
display: flex; list-style: none; max-width: 1080px; margin: 0 auto; padding: 10px 0;
}
.menu-list li { margin-right: 20px; font-weight: bold; font-size: 15px; }
.content { max-width: 1080px; margin: 20px auto; display: flex; gap: 20px; }
.left-section { flex: 2; background: #fff; height: 400px; border: 1px solid #e4e7e8; padding: 20px; }
.right-section { flex: 1; }
.login-box { background: #fff; border: 1px solid #e4e7e8; padding: 20px; }
.user-name { display: block; margin-bottom: 5px; }
.user-email { font-size: 12px; color: #888; }
.logout-btn {
width: 100%; margin-top: 15px; padding: 10px;
background: var(--naver-bg); border: 1px solid #e4e7e8; cursor: pointer;
}
</style>
src/asserts/main.css
:root {
--naver-green: #03cf5d;
--naver-bg: #f5f6f7;
}
body {
margin: 0;
background-color: var(--naver-bg);
font-family: -apple-system, BlinkMacSystemFont, "Malgun Gothic", Dotum, sans-serif;
}
굳이 css를 해당 vue 안에 넣어주지 않아도,
main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
main.ts 에 import ./style.css가 설정되어 있으면 Vite가 빌드할 때 이 파일을 읽어서 프로젝트 전체(모든 컴포넌트)에 적용해 준다고 한다.

실행하면 이런 모양으로 된다.
근데 다크모드 옵션이 있는 것 같은데, 다크모드에선 이런 식으로 보인다.

여기서 갑자기 궁금증이 생겼다.
1. style.css 와 App.vue 중 우선순위
2. .header와 .header-inner 이 다른 건 알겠지만, .header 은 style.css 에 적용하고 .header-inner은 App.vue에 있을 때 둘다 적용되나?
3. <section> 과 <aside> 차이
1. style.css vs App.vue 충돌 우선순위
- 결론: 기본적으로 나중에 로드된 스타일이 우선권을 갖습니다. main.ts에서 style.css를 먼저 임포트하고 그 뒤에 App.vue를 실행하기 때문에, 보통 App.vue 안의 스타일이 더 강력하게 적용됩니다.
- 하지만 body 태그는 프로젝트 전체의 뿌리이므로, 전역 설정인 style.css에 한 번만 적고 App.vue에서는 지우는 것이 관리에 훨씬 좋습니다.
2. .header와 .header-inner의 차이
- 구조적 차이: .header는 화면 끝에서 끝까지 이어지는 긴 띠이고, .header-inner는 그 안에서 콘텐츠를 가운데로 모아주는 바구니라고 생각하시면 됩니다.
- 중첩 적용: 네, 둘 다 적용됩니다! .header에 배경색을 주고 .header-inner에 너비를 주면, 전체적으로 색이 칠해진 긴 띠 안에 내용물만 정해진 너비로 모이게 됩니다.
3. section 과 aside 차이
- section: 문서의 주요 본문 영역.
- aside: 본문 옆에 부수적으로 붙는 사이드바
이건 시맨틱 태그(Semantic Tag) 라는 개념이다. 컴퓨터(브라우저)에게 이 영역이 어떤 의미인지 알려주는 이름표
다시 돌아와서 다크모드일 떄와 라이트모드일 때 각각 색을 지정해 주었다.
기존에는 자동 변환 같은 기능을 사용한 것 같았다.
App.vue
<template>
<div class="naver-clone">
<header class="header">
<div class="header-inner">
<h1 class="logo">NAVER</h1>
<div class="search-area">
<input type="text" class="search-input" placeholder="검색어를 입력해 주세요." />
<button class="search-btn">🔍</button>
</div>
</div>
</header>
<nav class="gnb">
<ul class="menu-list">
<li>메일</li><li>카페</li><li>블로그</li><li>쇼핑</li><li>뉴스</li>
</ul>
</nav>
<main class="content">
<section class="left-section">
<div class="news-stand">뉴스스탠드 영역 (준비 중)</div>
</section></main>
</div>
</template>
<style scoped>
/* 헤더 & 메뉴바 영역 */
.header, .gnb {
background-color: var(--bg-card);
border-bottom: 1px solid var(--border-line);
}
.logo { color: var(--naver-green); font-weight: bold; font-size:30px; flex-shrink: 0;}
.menu-list { display: flex; list-style: none; gap: 20px; padding: 10px 0; }
.menu-list li {
color: var(--color-sub); /* 라이트일 땐 회색, 다크일 땐 흰색에 가깝게 */
font-weight: bold;
}
/* 검색창 영역 (이미지에서 가장 어색했던 부분) */
.search-area {
flex: 1;
margin: 0 auto;
width: 100%;
max-width: 600px;
border: 2px solid var(--naver-green);
background-color: var(--bg-card); /* 박스 배경색 따라가기 */
border-radius: 2px;
}
.search-input {
border: none;
padding: 13px;
flex: 1;
outline: none;
background-color: transparent; /* 배경을 투명하게 해서 부모 색상 노출 */
color: var(--color-main); /* 글자색이 배경에 맞춰 자동 조절됨 */
}
.search-btn { background: var(--naver-green); border: none; padding: 0 15px; cursor: pointer; }
/* 메인 콘텐츠 영역 */
.content { display: flex; gap: 20px; max-width: 1130px; margin: 20px auto; }
.left-section, .login-box {
background-color: var(--bg-card);
border: 1px solid var(--border-line);
padding: 20px;
color: var(--color-main);
}
.header-inner, .inner {
width: 90%; /* 화면 가로의 90% 사용 */
max-width: 1400px; /* 너무 무한정 커지는 것 방지 (네이버보다 조금 더 넓게 설정) */
margin: 0 auto; /* 가운데 정렬 */
display: flex;
align-items: center;
padding: 20px 0;
gap: 10px; /* 로고와 검색창 사이의 간격을 직접 조절 */
}
/* 2. 메인 콘텐츠 영역 너비 조절 */
.content {
width: 90%; /* 화면 가로의 90% 사용 */
max-width: 1400px;
margin: 20px auto; /* 위아래 여백 20px, 좌우 가운데 정렬 */
display: flex;
gap: 20px; /* 왼쪽 콘텐츠와 오른쪽 사이드바 간격 */
}
/* 3. 왼쪽과 오른쪽 비율 조정 */
.left-section {
flex: 3; /* 왼쪽(뉴스 등)이 3만큼 차지 */
}
.right-section {
flex: 1; /* 오른쪽(로그인 등)이 1만큼 차지 */
min-width: 300px; /* 로그인 박스가 너무 작아지지 않게 최소 너비 고정 */
}
</style>
src/style.css
/* 기본 테마 (라이트 모드) */
:root {
--bg-body: #f5f6f7; /* 전체 배경 */
--bg-card: #ffffff; /* 박스 배경 */
--color-main: #080808; /* 메인 글자색 */
--color-sub: #666666; /* 보조 글자색 */
--border-line: #ebebeb; /* 테두리 색 */
--naver-green: #03cf5d;
}
/* 시스템이 다크모드일 때 자동으로 교체될 값 */
@media (prefers-color-scheme: dark) {
:root {
--bg-body: #101012; /* 아주 어두운 배경 */
--bg-card: #1e1f21; /* 어두운 회색 박스 */
--color-main: #f5f6f7; /* 밝은 글자색 */
--color-sub: #a0a0a0; /* 연한 회색 글자 */
--border-line: #303033; /* 어두운 테두리 */
}
}
/* 모든 화면에 공통 적용 */
html, body {
background-color: var(--bg-body) !important;
color: var(--color-main) !important;
margin: 0;
padding: 0;
transition: all 0.2s ease; /* 색상 변할 때 부드럽게 */
width: 100%;
height: 100%;
}
.search-area {
display: flex;
border: 2px solid var(--naver-green);
background-color: var(--bg-card);
width: 100%; /* 부모 요소(header-inner) 안에서 꽉 차게 */
max-width: 700px; /* 최대 700px까지만 커지도록 설정 */
margin: 0 20px; /* 로고와 메뉴 사이 여백 */
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
height: 100%;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
3. 로그인 구현
App.vue
<aside class="right-section">
<aside class="right-section">
<div v-if="!user" class="login-box before-login">
<div class="login-inputs">
<input v-model="userId" type="text" placeholder="아이디" class="input-field" />
<input v-model="userPw" type="password" placeholder="비밀번호" class="input-field" />
</div>
<button @click="handleLogin" class="login-btn">로그인</button>
<div class="login-sub">
<span>아이디 찾기</span> | <span>비밀번호 찾기</span> | <span>회원가입</span>
</div>
</div>
<div v-else class="login-box after-login">
<div class="user-info">
<strong>{{ user.userName }}</strong>님 환영합니다!
<p>{{ user.email }}</p>
</div>
<button @click="handleLogout" class="logout-btn">로그아웃</button>
</div>
</aside>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
const user = ref(null)
// 로그아웃 함수: user 변수를 비우면 화면이 자동으로 로그인 전으로 바뀜
const handleLogout = () => {
user.value = null
alert('로그아웃 되었습니다.')
}
// 로그인 함수: 버튼 클릭 시 백엔드에서 데이터를 새로 가져옴
const handleLogin = async () => {
try {
const response = await axios.get('/api/user/naver_test')
if (response.data) {
user.value = response.data
} else {
alert('사용자 정보를 찾을 수 없습니다.')
}
} catch (error) {
console.error('로그인 에러:', error)
alert('백엔드 서버 연결을 확인하세요!');
}
}
// 처음 로드될 때는 로그인이 안 된 상태로 두고 싶다면
// 아래 onMounted 안의 내용을 주석 처리하거나 지우세요.
onMounted(async () => {
// 초기 로딩 시 자동으로 데이터를 가져오고 싶지 않다면 이 안을 비워두세요.
})
</script>
.login-box {
background-color: var(--bg-card);
border: 1px solid var(--border-line);
padding: 20px;
text-align: center;
}
.login-msg {
font-size: 13px;
margin-bottom: 15px;
color: var(--color-sub);
}
.login-btn {
width: 100%;
padding: 15px;
background-color: var(--naver-green);
color: white;
border: none;
font-weight: bold;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
}
.login-sub {
margin-top: 15px;
font-size: 12px;
color: var(--color-sub);
}
.logout-btn {
margin-top: 10px;
width: 100%;
padding: 8px;
background: var(--bg-body);
border: 1px solid var(--border-line);
cursor: pointer;
color: var(--color-main);
}
ALTER TABLE USERS ADD (USER_PW VARCHAR2(255));
UPDATE USERS SET USER_PW = '1234' WHERE USER_ID = 'naver_test';
COMMIT;
com.naver.clone.dto.UserDto.java
@Data
public class UserDto {
private String userId;
private String userName;
private String email;
private String userPw; // 이 부분이 추가되어야 합니다!
}
src/main/resources/mapper/UserMapper.xml
<select id="loginCheck" parameterType="com.naver.clone.dto.UserDto" resultType="com.naver.clone.dto.UserDto">
SELECT USER_ID, USER_NAME, EMAIL, USER_PW
FROM USERS
WHERE USER_ID = #{userId}
AND USER_PW = #{userPw}
</select>
com.naver.clone.mapper.UserMapper.java
public interface UserMapper {
UserDto getUserById(String id);
UserDto loginCheck(UserDto loginRequest);
}
com.naver.clone.controller.UserController.java
@RequestMapping("/api/user") // 2. 모든 주소 앞에 "/api"를 붙임
public class UserController {
private final UserMapper userMapper;
// 3. 생성자를 통해 Mapper를 주입받음 (DB 연결 통로 확보)
public UserController(UserMapper userMapper) {
this.userMapper = userMapper;
}
@PostMapping("/login") // 보안을 위해 POST 방식을 사용
public UserDto login(@RequestBody UserDto loginRequest) {
UserDto user = userMapper.loginCheck(loginRequest);
return user;
}
}

1. 생성자 주입 vs 필드 주입 (@Autowired)
스프링에서 Mapper나 Service 같은 부품을 가져다 쓸 때 사용하는 두 가지 방식입니다.
- 필드 주입 (어노테이션 방식): 변수 위에 @Autowired만 붙이는 방식입니다. 코드가 짧아 보이지만, 외부에서 가짜 부품(Test 코드 등)을 끼워 넣기가 어렵고 스프링에 너무 의존적이라는 단점이 있습니다.
- 생성자 주입 (지민 님이 사용 중인 final 방식): 변수를 final로 선언하고 생성자를 통해 받아오는 방식입니다. 최근 스프링에서 가장 권장하는 방식입니다.
- 안전성: final이기 때문에 한 번 조립된 부품이 중간에 바뀔 염려가 없습니다.
- 필수성: 생성 시점에 부품이 없으면 아예 에러가 나므로, 실수로 빈 부품을 쓰는 일을 막아줍니다.
2. Axios가 정확히 하는 일
Axios는 브라우저(프론트엔드)에서 백엔드 서버와 데이터를 주고받기 위해 사용하는 **통신 도구(라이브러리)**입니다. 지민 님이 1월 26일에 IntelliJ와 Vite 환경을 세팅하면서 설치하셨던 그 도구죠.
주요 명령어는 다음과 같습니다:
- axios.get: "서버야, 저 데이터 좀 가져다줘." (주로 데이터 조회 시 사용)
- axios.post: "서버야, 이 데이터 보낼 테니까 처리해줘." (주로 로그인, 글쓰기 등 데이터를 담아 보낼 때 사용)
- axios.put: "서버야, 기존 데이터 이거로 수정해줘."
- axios.delete: "서버야, 이 데이터 삭제해줘."
'Project' 카테고리의 다른 글
| [NAVER CLONE #1] 네이버 클론코딩 환경 설정 (0) | 2026.01.26 |
|---|---|
| [바이브 코딩] 비개발자 AI 바이브코딩 강의 #00 (1) | 2025.09.08 |
| [javascript] 네이버 클론 코딩 + 블로그 5: 게시글 수정 (0) | 2025.03.13 |
| [javascript] 네이버 클론 코딩 + 블로그 5: 답글 & 공감 (0) | 2025.03.12 |
| [javascript] 네이버 클론 코딩 + 블로그 4: JWT 발급 (0) | 2025.03.12 |