Syw.Frontend

๐Ÿงฉ Vue 2๋กœ ๋ฐฐ์šฐ๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๊ธฐ์ดˆ

2๋‹จ๊ณ„. ๋„ฅ์ŠคํŠธ๊ณต๋ฌด์› ๋ฉ”์ธ Vue + Express๋กœ ๋กœ ํด๋ก ํ•˜๊ธฐ

2-3. ๋ฉ”์ธ ํƒญ + ์Šฌ๋ผ์ด๋“œ ์—ฐ๋™ UI ๊ตฌํ˜„

๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ

  • ๋ฉ”๊ฐ€๊ณต๋ฌด์› ๋ฉ”์ธ์ฒ˜๋Ÿผ ํƒญ ํด๋ฆญ ์‹œ ์ฝ˜ํ…์ธ ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋Š” ์Šฌ๋ผ์ด๋“œ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  • MainTabs.vue ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— Swiper๋ฅผ ์—ฐ๋™ํ•˜๊ณ , ์„ ํƒ๋œ ํƒญ์— ๋”ฐ๋ผ ์Šฌ๋ผ์ด๋“œ ์ฝ˜ํ…์ธ ๊ฐ€ ๋™์ ์œผ๋กœ ๋ฐ”๋€Œ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

๐Ÿ“ ํŒŒ์ผ ๊ตฌ์กฐ

src/
โ”œโ”€โ”€ components/
โ”‚   โ””โ”€โ”€ MainTabs.vue
โ”‚   โ””โ”€โ”€ TabSlide.vue โ† ์ƒˆ๋กœ ์ƒ์„ฑ

๐Ÿงฉ 1. MainTabs.vue ๊ตฌํ˜„

<template>
  <div class="main-tabs">
    <h3 class="title">๋„ฅ์ŠคํŠธ๊ณต๋ฌด์› 1์œ„ ๊ฐ•์‚ฌ์ง„</h3>
    <div class="tab-slide">
      <div class="tabs">
        <button
          v-for="(tab, idx) in tabs"
          :key="idx"
          :class="{ active: currentTab === idx }"
          @click="changeTab(idx)"
        >
          {{ tab.label }}
        </button>
      </div>

      <TabSlide v-if="mounted" :key="renderKey" :slides="tabs[currentTab].slides" />
    </div>
  </div>
</template>

<script>
import TabSlide from "./TabSlide.vue";

export default {
  components: { TabSlide },
  data() {
    return {
      currentTab: 0,
      renderKey: 0,
      mounted: true,
      tabs: [
        {
          label: "๊ตญ์–ด",
          slides: [
            {
              img: "https://img.megagong.net/profphoto/gong/gilltoraebi/main_intro.png",
              alt: "์ด์œค์ฃผ",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/jhssam1003/main_intro.png",
              alt: "์ž„์ง€ํ˜œ",
            },
          ],
        },
        {
          label: "์˜์–ด",
          slides: [
            {
              img: "https://img.megagong.net/profphoto/gong/tjenglish08/main_intro.png",
              alt: "์กฐํƒœ์ •",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/megamega2/main_intro.png",
              alt: "์„ฑ์ •ํ˜œ",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/incredvoc/main_intro.png",
              alt: "๊ฒฝ์„ ์‹",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/nojoony1/main_intro.png",
              alt: "๋ฐ•๋…ธ์ค€",
            },
          ],
        },
        {
          label: "ํ•œ๊ตญ์‚ฌ",
          slides: [
            {
              img: "https://img.megagong.net/profphoto/gong/gosabu88/main_intro.png",
              alt: "๊ณ ์ข…ํ›ˆ",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/vision0911/main_intro.png",
              alt: "๋ผ์˜ํ™˜",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/duwo20405/main_intro.png",
              alt: "์ตœ์˜์žฌ",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/jeonhangil/main_intro.png",
              alt: "์ „ํ•œ๊ธธ",
            },
          ],
        },
        {
          label: "ํ–‰์ •ํ•™",
          slides: [
            {
              img: "https://img.megagong.net/profphoto/gong/plower3362/main_intro.png",
              alt: "ํ™ฉ์ฒ ๊ณค",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/happy0308/main_intro.png",
              alt: "์ด์ƒํ—Œ",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/shin242/main_intro.png",
              alt: "์‹ ์šฉํ•œ",
            },
          ],
        },
        {
          label: "ํ–‰์ •๋ฒ•",
          slides: [
            {
              img: "https://img.megagong.net/profphoto/gong/allawyer/main_intro.png",
              alt: "์œ ํœ˜์šด",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/wjsgywls1/main_intro.png",
              alt: "์ „ํšจ์ง„",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/geek2991/main_intro.png",
              alt: "์ •์ธ๊ตญ",
            },
            {
              img: "https://img.megagong.net/profphoto/gong/ysw7/main_intro.png",
              alt: "์–‘์Šน์šฐ",
            },
          ],
        },
      ],
    };
  },
  methods: {
    changeTab(index) {
      this.currentTab = index;
      this.mounted = false;
      this.$nextTick(() => {
        this.renderKey++;
        this.mounted = true;
      });
    },
  },
};
</script>

<style scoped lang="scss">
.main-tabs {
  width: 790px;
  .title {
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 16px;
  }

  .tab-slide {
    .tabs {
      display: flex;
      gap: 12px;
      margin-bottom: 20px;

      button {
        background: #f2f2f2;
        border: none;
        padding: 8px 16px;
        border-radius: 6px;
        cursor: pointer;
        font-weight: 500;
        color: #333;

        &.active {
          background: #0055ff;
          color: #fff;
        }
      }
    }
  }
}
</style>

๐Ÿงฉ 2. TabSlide.vue ๊ตฌํ˜„

Swiper๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Šฌ๋ผ์ด๋“œ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

<template>
  <swiper ref="mySwiper" :options="swiperOption" class="swiper-container">
    <swiper-slide v-for="(item, i) in slides" :key="i" class="teacher-slide">
      <div class="teacher-box">
        <img :src="item.img" :alt="item.alt" />
        <p class="teacher-name">{{ item.alt }}</p>
      </div>
    </swiper-slide>
    <div class="swiper-pagination" slot="pagination"></div>
  </swiper>
</template>

<script>
export default {
  props: {
    slides: {
      type: Array,
      required: true,
    },
  },
  data() {
    return {
      swiperOption: {
        slidesPerView: 4,
        spaceBetween: 10,
        pagination: {
          el: ".swiper-pagination",
        },
        loop: false,
      },
    };
  },
  watch: {
    slides() {
      this.$nextTick(() => {
        const swiper = this.$refs.mySwiper?.swiper;
        if (swiper) {
          swiper.update();
          swiper.slideTo(0);
        }
      });
    },
  },
};
</script>

<style scoped>
.swiper-container {
  width: 100%;
  overflow: hidden;
}

.swiper-slide {
  /* width: auto !important; */
  flex-shrink: 0;
  display: flex;
  justify-content: center;
}

.teacher-box {
  width: 100%;
  max-width: 200px;
  background: #fff;
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 12px;
  text-align: center;
}

.teacher-box:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.teacher-box img {
  width: 100%;
  height: auto;
  border-radius: 4px;
  margin-bottom: 8px;
}

.teacher-name {
  font-size: 14px;
  font-weight: 600;
  color: #333;
}
</style>

โœ… ์‹คํ–‰ ๊ฒฐ๊ณผ

  • ์ƒ๋‹จ ํƒญ์„ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹นํ•˜๋Š” ์Šฌ๋ผ์ด๋“œ ์ฝ˜ํ…์ธ ๊ฐ€ ์ž๋™์œผ๋กœ ๋ฐ”๋€๋‹ˆ๋‹ค.
  • ๊ฐ๊ฐ์˜ ์Šฌ๋ผ์ด๋“œ๋Š” Swiper๋กœ ์Šฌ๋ผ์ด๋“œ ์ด๋™์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๐ŸŽฏ ๋‹ค์Œ ๊ณผ์ œ

์•„๋ž˜ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”!

  1. ๊ฐ ์Šฌ๋ผ์ด๋“œ ์•„์ดํ…œ์— ๊ณผ๋ชฉ ํ…์ŠคํŠธ๋„ ํ•จ๊ป˜ ์ถœ๋ ฅ

    ์˜ˆ์‹œ:

    { img: "...", alt: "๋ผ์˜ํ™˜", subject: "ํ•œ๊ตญ์‚ฌ" }
    
  2. ๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ (768px ์ดํ•˜์—์„œ ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์ค„์ด๊ธฐ)

    Swiper ์˜ต์…˜์—์„œ breakpoints ์„ค์ • ์ถ”๊ฐ€.

๐Ÿ’ฌ ๋Œ“๊ธ€

    โ€ป ๋กœ๊ทธ์ธ ํ›„ ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.