Skip to content

toosign00/toosign00.github.io

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

프론트엔드 개발자 노현수 포트폴리오



📚 목차



📝 프로젝트 소개

개인 포트폴리오 웹사이트입니다. 헤더 메뉴 클릭 시 스크롤 이동 기능을 구현하였고, 스킬 버튼을 클릭하면 해당 스킬의 숙련도를 타이핑 효과로 표시합니다. 또한, 프로젝트 모달 창을 통해 상세 설명을 확인할 수 있습니다.

성능 최적화, 웹표준, 접근성, SEO 최적화를 고려하여 개발하였으며, Google Lighthouse 평가에서 모든 항목에서 100점을 기록하였습니다.

개요

프로젝트명 : 포트폴리오 웹사이트

프로젝트 기간 : 202412 ~ 진행 중

인원 : 개인

프로젝트 목적 : 개인 포트폴리오

배포 URL : https://toosign.kr

GitHub : 저장소 링크



🛠 기술 스택

Front-end

HTML5 CSS3 JavaScript

Tools & Deployment

Git Vercel

Development Environment

Visual Studio Code



🗂 프로젝트 구조

📦 포트폴리오
 ┣ 📂.vercel              # Vercel 배포 설정
 ┣ 📂.vscode              # VS Code 설정
 ┃ ┗ 📜settings.json
 ┣ 📂assets              # 정적 자원 관리
 ┃ ┣ 📂css               # 스타일시트 파일
 ┃ ┣ 📂images            # 이미지 파일
 ┃ ┣ 📂js                # 자바스크립트 파일
 ┃ ┗ 📂pdf               # PDF 문서
 ┣ 📜.gitignore          # Git 제외 파일 설정
 ┣ 📜index.html          # 메인 HTML 파일
 ┣ 📜project.json        # 프로젝트 설정
 ┣ 📜README.md           # 프로젝트 문서
 ┣ 📜robots.txt          # 검색 엔진 크롤링 설정
 ┗ 📜sitemap.xml         # 사이트맵


✨ 주요 기능

기능 설명
라이트/다크모드 🌗 라이트/다크 모드

data-theme 속성을 이용한 테마 전환 구현
localStorage로 사용자 테마 설정 유지
• DOM 로드 전 테마 적용으로 깜빡임 현상 방지

👉 코드 보기
스크롤 📜 부드러운 스크롤

• 헤더 메뉴 클릭 시 부드러운 스크롤 이동
• CSS scroll-behavior: smooth 활용
• 사용자 경험 향상을 위한 자연스러운 애니메이션

👉 코드 보기
타이핑 ⌨️ 타이핑 효과

• 스킬 버튼 클릭 시 타이핑 애니메이션 구현
• TypeWriter 클래스를 활용한 모듈화
• 중복 클릭 방지 로직 구현

👉 코드 보기
모달 🔲 모달창 팝업

• ProjectModal 클래스로 동적 생성
• 키보드 접근성 고려 (ESC 키 지원)
• 프로젝트 정보 동적 렌더링

👉 코드 보기
Lighthouse 점수
📊 성능 최적화

• loading="lazy" 속성을 통한 이미지 지연 로딩
• 시멘틱 태그를 활용한 웹표준 준수
• alt, aria-label 적용으로 접근성 개선
• Meta Data, Open Graph, robots.txt, sitemap.xml 적용

👉 Google Lighthouse 전 항목 100점 달성


💻 주요 코드

라이트 / 다크모드 구현

<!-- head 태그 내에 Blocking JS 적용 -->
    <script>
      // 페이지 로드 전에 즉시 테마 적용
      (function () {
        const savedTheme = localStorage.getItem('theme') || 'light';
        document.documentElement.setAttribute('data-theme', savedTheme);
      })();
document.addEventListener('DOMContentLoaded', function () {
  const savedTheme = localStorage.getItem('theme') || 'light';
  const buttonText = document.querySelector(".nav--button-text");
  const buttonIcon = document.querySelector(".nav--button-icon");

  // UI 요소만 업데이트
  updateThemeUI(savedTheme);

  // 테마 전환 버튼 이벤트 리스너
  document.getElementById("theme-toggle").addEventListener("click", function () {
    const currentTheme = document.documentElement.getAttribute("data-theme");
    const newTheme = currentTheme === "dark" ? "light" : "dark";

    // 테마 속성 설정 및 로컬 스토리지에 저장
    document.documentElement.setAttribute("data-theme", newTheme);
    localStorage.setItem('theme', newTheme);
    updateThemeUI(newTheme);
  });

  // UI 업데이트 함수
  function updateThemeUI(theme) {
    buttonText.textContent = theme === "dark" ? "Dark" : "Light";
    buttonIcon.src = theme === "dark"
      ? "./assets/images/dark-mode-icon.svg"
      : "./assets/images/light-mode-icon.svg";
  }
});

부드러운 스크롤

document.addEventListener("DOMContentLoaded", () => {
  // 네비게이션 링크와 로고 링크 선택
  const navLinks = document.querySelectorAll(".nav--link");
  const logoLink = document.getElementById("logo-link");
  const headerOffset = document.querySelector("header").offsetHeight;

  // 부드러운 스크롤 함수
  const smoothScroll = (e) => {
    e.preventDefault();
    const targetId = e.currentTarget.getAttribute("href").substring(1);
    const targetSection = document.getElementById(targetId);

    if (targetSection) {
      window.scrollTo({
        top: targetSection.offsetTop - headerOffset,
        behavior: "smooth"
      });
    }
  };

  // 네비게이션 링크들에 이벤트 리스너 추가
  navLinks.forEach(link => {
    link.addEventListener("click", smoothScroll);
  });

  // 로고 링크에 이벤트 리스너 추가
  logoLink.addEventListener("click", smoothScroll);
});

타이핑 효과

document.addEventListener("DOMContentLoaded", () => {
  // 각 스킬 박스에 대한 설명을 담은 객체 생성
  const skillDescriptions = {
    "html-box": "HTML5 문서 구조를 이해하며, 시멘틱 코드를 사용하고, SEO 적용을 고려하여 최적화한 경험이 있습니다.",
    "css-box": "Media Query, CSS3, Flex-box와 CSS Grid에 능숙하며, CSS 방법론을 사용해 재사용성과 유지보수성을 고려한 코드를 작성할 수 있습니다.",
    "js-box": "JavaScript ES6 이상 문법과 DOM 조작을 활용해 동적 웹페이지를 구현하며, Stack, Queue, Event Loop, Heap 등의 동작 원리와 비동기 처리(Callback, async/await, Promise)에 대한 이해를 갖추고 있습니다.",
    "node-box": "Node.js와 Express.js를 활용해 서버를 구축하고, npm으로 라이브러리를 관리하며, MongoDB와의 연동이 가능합니다.",
    "git-box": "Git을 활용한 버전 관리와 branch, merge, rebase를 통한 협업에 능숙하며, 다양한 브랜치 전략(Git flow, Trunk-based)을 적용할 수 있습니다.",
    "github-box": "GitHub를 사용해 원격 저장소를 관리하고, Pull Request 기반의 코드 리뷰와 브랜치 보호 규칙 설정을 통해 협업 프로세스를 최적화한 경험이 있습니다.",
    design: "Figma와 Adobe 디자인 툴을 활용하며, UI/UX 디자인 프로세스에 대한 이해를 바탕으로 사용자 중심의 디자인을 구현할 수 있습니다.",
  };

  // 타이핑 효과를 구현하는 클래스
  class TypeWriter {
    // 생성자: 텍스트를 표시할 DOM 요소와 타이핑 timeout ID 초기화
    constructor(element) {
      this.element = element;
      this.typingTimeout = null;
    }

    // 텍스트를 타이핑 효과로 출력하는 메서드
    type(text, speed = 10) {
      // 같은 텍스트가 이미 있다면 지우고 다시 시작
      if (this.element.textContent === text) {
        this.element.textContent = "";
      }

      // 진행 중인 타이핑이 있다면 중지
      clearTimeout(this.typingTimeout);
      this.element.textContent = "";

      // 타이핑할 현재 문자의 위치
      let index = 0;

      // 한 글자씩 타이핑하는 함수
      const typeChar = () => {
        if (index < text.length) {
          this.element.textContent += text.charAt(index);
          index++;
          this.typingTimeout = setTimeout(typeChar, speed);
        }
      };

      // 타이핑 시작
      typeChar();
    }
  }

  // 설명 텍스트를 표시할 요소에 대한 타이핑 인스턴스 생성
  const typewriter = new TypeWriter(document.querySelector(".skill--description-text"));

  // 모든 스킬 박스에 클릭 이벤트 설정하는 함수
  const initializeSkillBoxes = () => {
    // 일반 스킬 박스들 이벤트 설정
    Object.keys(skillDescriptions).forEach((id) => {
      if (id === "design") return; // 디자인은 따로 처리

      const element = document.getElementById(id);
      if (element) {
        element.addEventListener("click", () => typewriter.type(skillDescriptions[id]));
      }
    });

    // 디자인 스킬 박스들 이벤트 설정
    document.querySelectorAll(".design-box").forEach((box) => {
      box.addEventListener("click", () => typewriter.type(skillDescriptions["design"]));
    });
  };

  // 페이지 로드 시 초기화 실행
  initializeSkillBoxes();
});

모달창 팝업 효과

// ProjectModal 클래스 정의
class ProjectModal {
  constructor() {
    this.modal = document.getElementById("projectModal");
    this.modalClose = this.modal.querySelector(".modal--close");
    this.modalOverlay = this.modal.querySelector(".modal--overlay");
    this.bindEvents();
  }

  bindEvents() {
    this.modalClose.addEventListener("click", () => this.close());
    this.modalOverlay.addEventListener("click", () => this.close());
    document.addEventListener("keydown", (e) => {
      if (e.key === "Escape" && this.isActive()) {
        this.close();
      }
    });
  }

  open(projectId) {
    const project = projects[projectId];
    if (!project) return;

    this.updateContent(project);
    this.modal.classList.add("active");
    document.body.style.overflow = "hidden";
  }

  close() {
    this.modal.classList.remove("active");
    document.body.style.overflow = "";
  }

  isActive() {
    return this.modal.classList.contains("active");
  }

  updateContent(project) {
    this.modal.querySelector(".modal--title").textContent = project.title;
    this.modal.querySelector(".modal--duration").textContent = project.duration;
    this.updateTechnologies(project.technologies);
    this.modal.querySelector(".modal--description").textContent = project.description;
    this.updateLinks(project);
  }

  updateTechnologies(technologies) {
    const techContainer = this.modal.querySelector(".modal--technologies");
    techContainer.innerHTML = technologies
      .map(
        (tech) => `
        <img 
          class="modal--tech-img" 
          src="${tech.imgSrc}" 
          alt="${tech.name}"
          title="${tech.name}"
        >
      `
      )
      .join("");
  }

  updateLinks(project) {
    const githubLink = this.modal.querySelector(".github-link");
    const deployLink = this.modal.querySelector(".deploy-link");

    githubLink.href = project.github.url;
    githubLink.setAttribute("aria-label", project.github.ariaLabel);

    // 배포 링크가 없는 경우 숨김 처리, 있으면 보이도록 설정 node.js 프로젝트는 deploy 링크가 없음, 추후 AWS를 사용하여 배포 후 링크 추가 예정
    if (project.deploy) {
      deployLink.style.display = "inline-block";
      deployLink.href = project.deploy.url;
      deployLink.setAttribute("aria-label", project.deploy.ariaLabel);
    } else {
      deployLink.style.display = "none";
    }
  }
}

// 초기화
document.addEventListener("DOMContentLoaded", () => {
  const projectsManager = new ProjectsManager();
  projectsManager.renderProjects();

  const modal = new ProjectModal();

  // 프로젝트 클릭 이벤트 위임
  document.querySelector(".projects--grid").addEventListener("click", (e) => {
    const projectItem = e.target.closest(".project--item");
    if (projectItem) {
      modal.open(projectItem.dataset.projectId);
    }
  });
});


💡 문제 해결

1. 테마 전환 시 깜빡임 현상 (FOUC)

문제 상황

  • 페이지 새로고침 시 라이트/다크 테마가 늦게 적용되어 깜빡임 현상 발생
  • 기존 코드는 DOM이 로드된 후에 테마를 적용하여 사용자 경험 저하

해결 방법

<!-- head 태그 내에 Blocking JS 적용 -->
<script>
  (function () {
    const savedTheme = localStorage.getItem('theme') || 'light';
    document.documentElement.setAttribute('data-theme', savedTheme);
  })();
</script>
  • DOM 로드 전에 즉시 실행되는 Blocking JS 구현
  • localStorage에서 사용자가 설정한 테마 정보를 가져와 즉시 적용
  • head 태그 내에 스크립트를 배치하여 페이지 렌더링 전에 테마 적용

개선 결과

  • 페이지 새로고침 시 깜빡임 현상 완전 제거
  • 부드러운 사용자 경험 제공

2. 연속적인 타이핑 효과 충돌

문제 상황

  • 스킬 버튼 연속 클릭 시 타이핑 효과들이 충돌하는 현상 발생
  • 이전 타이핑이 완료되기 전에 새로운 타이핑이 시작되어 텍스트가 깨지는 현상

해결 방법

class TypeWriter {
  constructor(element) {
    this.element = element;
    this.typingTimeout = null;  // timeout ID 관리 추가
  }

  type(text, speed = 10) {
    // 진행 중인 타이핑이 있다면 중지
    clearTimeout(this.typingTimeout);
    this.element.textContent = "";
    
    let index = 0;
    const typeChar = () => {
      if (index < text.length) {
        this.element.textContent += text.charAt(index);
        index++;
        this.typingTimeout = setTimeout(typeChar, speed);
      }
    };
    typeChar();
  }
}
  • TypeWriter 클래스에 timeout ID를 저장하여 관리하는 기능 추가
  • 새로운 타이핑 시작 전 진행 중인 타이핑을 중단하는 로직 구현
  • 텍스트 초기화 후 새로운 타이핑 시작

개선 결과

  • 연속 클릭 시에도 타이핑 효과가 자연스럽게 동작
  • 사용자 인터랙션에 대한 즉각적인 반응성 확보

향후 개선 계획

  1. UI/UX 개선

    • alert 대신 모달 또는 토스트 메시지로 알림 UI 개선
    • 로딩 상태에 대한 시각적 피드백 추가
  2. 반응형 웹 개선

    • 모바일 우선(Mobile First) 설계 방식 적용
    • 미디어 쿼리 브레이크포인트 최적화 (모바일, 태블릿, 데스크톱)

이러한 문제 해결 과정을 통해 사용자 경험을 개선하고, 코드의 안정성과 유지보수성을 향상시켰습니다.

📝 회고

😀 잘된 점 🤔 아쉬운 점
• 라이트/다크모드 깜빡임 현상 방지
• 성능, 웹표준, 접근성 최적화
• 동적 데이터 처리
• 반응형 디자인 미구현
• 프로젝트 설명 부족

About

포트폴리오 프로젝트 저장소

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published