Project

[NAVER CLONE #1] 네이버 클론코딩 Vue 화면 띄우기

끊임없이 개발하는 새럼 2026. 1. 28. 16:04

 


 

 

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: "서버야, 이 데이터 삭제해줘."