티스토리 뷰

반응형

Github page로 포트폴리오 웹페이지 만들기


About 페이지 만들기를 잠시 보류하고, Work페이지를 만드는 것을 먼저 해보도록 하겠습니다.

Work 페이지는 현재 만들어지고 있는 프로젝트 또는, 만들어 진 프로젝트들을 알파벳 순서로 보여 주고, 각각 링크를 달아 이동하겠끔 하는 것이 목표입니다.

되도록이면 프로젝들을 데모 형식으로 테스트 해볼수있는 페이지를 만들어 두는 것을 목표로 합니다.

이러한 목적을 가지고, 구현을 하기 위해 생각한 방법은 A to Z 알파벳 디렉토리를 만들고 그안에 프로젝트 디렉토리들을 넣어 index.js로 export 하는 방법을 생각했습니다.

이렇게 export 된 컴포넌트들을 필요한 곳에서는 @project 만 import 시켜서 loop로 돌리는 방식을 사용해볼까 합니다.

그럼 우선은 About 페이지와 같이 레이아웃을 먼저 구상해 봅니다.

Work 페이지도 About 페이지 처럼 단순하게 생각해 보았습니다.

좌측에는 각 프로젝트 알파벳으로 이동할 수 있는 Navigation이 있고, 메인 컨텐츠는 타이틀 , 그아래로 프로젝트 리스트들이 나열되어 있는 구성을 생각해 보았습니다.

이제 하나씩 구현 해 보도록 하겠습니다.

먼저 필요한 directory들을 생성해 주겠습니다.

1. Directory + index.js 파일 생성


조금은 무식한 방법일수도 있겠지만, 제 느낌대로 가보겠습니다.

src → project 디렉토리 안에 a-z 까지 디렉토리를 생성해 줍니다. 이와 함께 index.js도 생성해 줍니다.

마찬가지로 a-z 디렉토리 안에도 index.js 를 생성해 줍니다.

파일 구조는

src/project/index.js src/project/a/index.js

src/project/b/index.js 구조가 됩니다.

디렉토리안의 index.js를 통해 import … from ‘…’ 으로 모듈이나 컴포넌트를 불러올때, 파일이 포함된 디렉토리명으로 불러올수 있습니다.

이러한 구조를 통해서

src/project/index.js 에서는 각 알파벳 디렉토리를 모두 불러오고,

src/project/a/index.js

src/project/b/index.js

에서는 첫글자가 디렉토리의 알파벳에 해당하는 프로젝트를 그 안에 생성한뒤 불러 오는 방식을 취하겠습니다.

아직은 프로젝트들이 생성되어있지 않으므로 , 알파벳 디렉토리안의 index.js 에 빈 오브젝트를 default 로 export 하는 코드를 작성해 줍니다.

// src/project/{alphabet}/index.js

export default {}

src/project/index.js 에는 이 알파벳 디렉토리를 import 하는 코드를 작성해 줍니다.

// src/project/index.js

import * as a from "@project/a";
import * as b from "@project/b";
import * as c from "@project/c";
import * as d from "@project/d";
import * as e from "@project/e";
import * as f from "@project/f";
import * as g from "@project/g";
import * as h from "@project/h";
import * as i from "@project/i";
import * as j from "@project/j";
import * as k from "@project/k";
import * as l from "@project/l";
import * as m from "@project/m";
import * as n from "@project/n";
import * as o from "@project/o";
import * as p from "@project/p";
import * as q from "@project/q";
import * as r from "@project/r";
import * as s from "@project/s";
import * as t from "@project/t";
import * as u from "@project/u";
import * as v from "@project/v";
import * as w from "@project/w";
import * as x from "@project/x";
import * as y from "@project/y";
import * as z from "@project/z";

2. export default Project


src/project/index.js 에 각 프로젝트를 하나의 오브젝트로 만들어 export 시켜 주어야 합니다.

그러기 위해서는 먼저 import 된 디렉토리들을 객체로 만들어 주는 함수를 _unpack_ 이라고 이름 지어 줍니다. 이 함수는 import 된 모듈을 매개변수로 받아와,

{ 
	alphabet:{
						모듈이름:모듈(),
						모듈이름:모듈(),
						모듈이름:모듈(),
						...
						}
}

위와 같은 구조로 변환해 줍니다.

그럼 먼저 각각 import 된 * as Alphabet 이 어떤 형식으로 되어있는지 확인해 보겠습니다.

// src/project/index.js

import * as a from "@project/a";
...

console.log(a)

Module 로 정의된 객체가 있습니다.

이 객체를 위에서 구상한 객체로 변환하기 위해 Object.entries()reduce() 를 사용한 _unpack_ 함수를 정의해 줍니다.

const _unpack_ = (m) => {
  return Object.entries(m).reduce((o, k) => {
    o[k[0]] = k[1];
    return o;
  }, {});
};

이렇게 정의된 함수를 사용해 각각의 alphabet을 key값으로하고, unpack 된 결과값을 value로하는 객체인 Project를 만들어 줍니다.그리고 export 합니다.

// src/project/index.js

import * as a from "@project/a";
import * as b from "@project/b";
import * as c from "@project/c";
import * as d from "@project/d";
import * as e from "@project/e";
import * as f from "@project/f";
import * as g from "@project/g";
import * as h from "@project/h";
import * as i from "@project/i";
import * as j from "@project/j";
import * as k from "@project/k";
import * as l from "@project/l";
import * as m from "@project/m";
import * as n from "@project/n";
import * as o from "@project/o";
import * as p from "@project/p";
import * as q from "@project/q";
import * as r from "@project/r";
import * as s from "@project/s";
import * as t from "@project/t";
import * as u from "@project/u";
import * as v from "@project/v";
import * as w from "@project/w";
import * as x from "@project/x";
import * as y from "@project/y";
import * as z from "@project/z";

const _unpack_ = (m) => {
  return Object.entries(m).reduce((o, k) => {
    o[k[0]] = k[1];
    return o;
  }, {});
};

const Project = {
  a: _unpack_(a),
  b: _unpack_(b),
  c: _unpack_(c),
  d: _unpack_(d),
  e: _unpack_(e),
  f: _unpack_(f),
  g: _unpack_(g),
  h: _unpack_(h),
  i: _unpack_(i),
  j: _unpack_(j),
  k: _unpack_(k),
  l: _unpack_(l),
  m: _unpack_(m),
  n: _unpack_(n),
  o: _unpack_(o),
  p: _unpack_(p),
  q: _unpack_(q),
  r: _unpack_(r),
  s: _unpack_(s),
  t: _unpack_(t),
  u: _unpack_(u),
  v: _unpack_(v),
  w: _unpack_(w),
  x: _unpack_(x),
  y: _unpack_(y),
  z: _unpack_(z),
};

export default Project;

그렇다면 이 Project 는 어떤 결과값을 가지는지 확인해 보겠습니다.

// src/project/index.js
...

const Project = {
  a: _unpack_(a),
 ...
};
console.log(Project);
export default Project;

원하는 구조체로 나오는 것같습니다.

여기서 1가지 더 확인해 보기위해 a 디렉토리에 about이라는 프로젝트를 만들어 구조체에 추가하는 방식을 구현해 봅니다.

3. Add project & export


먼저 a 디렉토리 안에 about 디렉토리를 만들고, index.js파일을 그안에 추가해 줍니다.

각각의 프로젝트 디렉토리들에 있는 index.js는 프로젝트를 실행해 보는 데모페이지에 해당합니다.

src/project/a/about/index.js 에는 테스트용 About React 컴포넌트를 하나 만들어 주겠습니다.

// src/project/a/about/index.js

import React from 'react';

const About =()=>{
    return <></>
}

export default About

이제 이 컴포넌트를 src/project/a/index.js 에서 export 해주는 겁니다.

기존에 작성해 놓은 export default {} 를 주석 처리 해주고 그 아래에 적어 줍니다.

// src/project/a/index.js
// export default {};

export { default as About } from "@project/a/about";

그럼 이전에 src/project/index.js 에서 정의된 Project가 어떻게 변하는지 확인해 봅니다.

// src/project/index.js
...

const Project = {
  a: _unpack_(a),
 ...
};
console.log(Project);
export default Project;

default를 주석 처리해서 없어지고, About 에 해당하는 key-value가 생긴것을 확인 할 수 있습니다.

생각대로 잘 구현이 된것 같습니다.

이제 이 Project를 import 해서 사용해 보겠습니다.

먼저 이 프로젝트의 리스트들의 Route들을 생성해 주어야 합니다.

4. Route A to Z


Project 객체에 들어가있는 컴포넌트들은 각각 프로젝트 데모페이지입니다. 이러한 페이지에 접속하기 위해 Route를 생성해 path와 element를 등록해 주어야 합니다.

이런한 기능을 구현하기 위한 방법으로 Object.keys() 와 map()을 사용해 구현해 보겠습니다.

Route는 src/App.js에 정의되어있습니다.

해당 파일에 Project를 import 합니다.

// src/App.js
...
// components
import Project from "@project";
...

Routes 컴포넌트 안에 기능을 구현합니다.

// src/App.js
...

<Routes>
...
{Object.keys(Project).map((alphabet) => {
        let path = `/work/${alphabet}`;
        const components = Project[alphabet];
        return (
          <Fragment key={Math.random()}>
            {Object.keys(components).map((name) => {
              const ProjectComponent = components[name];
              path += `/${name.toLowerCase()}`;
              return (
                <Route
                  key={Math.random()}
                  exact
                  path={path}
                  element={
                    <Layout>
                      <ProjectComponent />
                    </Layout>
                  }
                />
              );
            })}
          </Fragment>
        );
      })}
...
</Routes>

path 는 ‘/work/<alphabet>/<project name>’

이 될수 있게끔했습니다.

key-value 쌍으로 되어있는 프로젝트 컴포넌트 들을 React Component로 사용하기 위해서 새로운 변수 ProjectComponent 에 담아 사용하였습니다.

모든 프로젝트에 동일하게 Layout이 적용 될수있게끔

ProjectComponent를 Layout으로 감싸줍니다.

5. Layout 적용 변경


About 페이지와 Work페이지 또한 공통된 Layout을 사용합니다.

그러므로 기존에 페이지 파일에 있는 Layout을 삭제 처리 하고, App.js에 작성된 main페이지를 제외한 각각 페이지의 Route의 element에 Layout을 넣어 줍니다.

// src/App.js
...
<Route
        exact
        path="/about"
        element={
          <Layout>
            <About />
          </Layout>
        }
      ></Route>
      <Route
        exact
        path="/work"
        element={
          <Layout>
            <Work />
          </Layout>
        }
      ></Route>
...

App.js 코드

// src/App.js

// modules
import React, { Fragment } from "react";
import { Routes, Route } from "react-router";

// css
import "@resources/css/app.css";

// Pages
import Main from "@pages/main";
import About from "@pages/about";
import Work from "@pages/work";

// components
import Layout from "@components/layout";

// project
import Project from "@project";

const App = () => {
  return (
    <Routes>
      <Route exact path="/" element={<Main />}></Route>
      <Route
        exact
        path="/about"
        element={
          <Layout>
            <About />
          </Layout>
        }
      ></Route>
      <Route
        exact
        path="/work"
        element={
          <Layout>
            <Work />
          </Layout>
        }
      ></Route>

      {Object.keys(Project).map((alphabet) => {
        let path = `/work/${alphabet}`;
        const components = Project[alphabet];
        return (
          <Fragment key={Math.random()}>
            {Object.keys(components).map((name) => {
              const ProjectComponent = components[name];
              path += `/${name.toLowerCase()}`;
              return (
                <Route
                  key={Math.random()}
                  exact
                  path={path}
                  element={
                    <Layout>
                      <ProjectComponent />
                    </Layout>
                  }
                />
              );
            })}
          </Fragment>
        );
      })}
    </Routes>
  );
};

export default App;

6. work 페이지 구현


이제 처음에 구상했던 레이아웃 형식에 맞춰 work 페이지를 구현해 보겠습니다.

가장 먼저 할일은 역시나 Project를 import 해주는 일입니다.

// src/pages/work/index.js

...

// project
import Project from "@project";
...

Route를 생성했던것과 마찬가지로 Object.keys()map()을 사용해 Component들을 만들어 주겠습니다.

work 페이지에 들어가는 item들은 크게 3가지로 볼수 있습니다.

프로젝트 네비게이션, 타이틀, 프로젝트 리스트 각각 style components 를 통해서 만들어 줍니다.

그리고 이 3가지 컴포넌트를 감싸는 WorkWrapper 라는 styled-components도 만들어 줍니다.

const WorkWrapper = styled.div``;
const ProjectNavigation = styled.div``;
const Title = styled.div``;
const ProjectList = styled.div``;

그리고 이 컴포넌트들을 Work에 반영합니다.

// src/pages/work/index.js

// modules
import React, { Fragment, useState, useCallback, useEffect } from "react";
import { Link } from "react-router-dom";

// project
import Project from "@project";
import styled from "styled-components";

const Work = () => {
 
  return <WorkWrapper>
    <ProjectNavigation></ProjectNavigation>
    <Title></Title>
    <ProjectList></ProjectList>
  </WorkWrapper>;
};

export default Work;

const WorkWrapper = styled.div``;
const ProjectNavigation = styled.div``;
const Title = styled.div``;
const ProjectList = styled.div``;

기본적으로 필요한 부분들을 세팅해 놓았습니다.

이준에서 가장 먼저 Title에 Projects List 라는 Text를 적어줍니다.

// src/pages/work/index.js
...
return (
    <WorkWrapper>
      <ProjectNavigation></ProjectNavigation>
      <Title>Projects List</Title>
      <ProjectList></ProjectList>
    </WorkWrapper>
  );
...

너무 조그마하네요.

이따가 스타일을 지정해 주겠습니다.

다음으로는 네비게이션을 구현해 줍니다.

Project Navigation

프로젝트 네비게이션은 좌측에 fix 되어 알파벳을 클릭하면 해당 알파벳으로 시작하는 프로젝트 리스트 부분으로 스크롤 되는 기능을 가지는 것을 생각했습니다.

먼저 현재 등록된 Project 객체는 key값으로 A to Z 까지의 알파벳을 가지고 있으므로 Object.keys()와 map()을 사용해 알파벳 대문자로 나타나겠끔 ProjectNavitionItems 라는 변수에 정의해 주고 . ProjectNavigation 컴포넌트안에 구현을 해 줍니다.

ProjectList

Route에서 사용했던것 과 같은 방식으로 ProjectListItems를 정의해 ProjectList에 안에 구현해 줍니다.

const Work = () => {
  const ProjectListItems = Object.keys(Project).map((alpha, i) => {
    let path = `/work/${alpha}`;

    return (
      <div id={alpha} className={i === 0 ? "selected" : ""} key={Math.random()}>
        <div className="project-list-title">
          <span>{alpha.toUpperCase()}</span>
        </div>
        <hr />
        <div className="project-list-item">
          {Object.keys(Project[alpha]).map((key) => {
            return (
              <>
                <Link to={`${path}/${key.toLowerCase()}`} key={Math.random()}>
                  <span>{key}</span>
                </Link>
              </>
            );
          })}
        </div>
      </div>
    );
  });
  const ProjectNavigationItems = Object.keys(Project).map((alpha) => {
    return (
      <div
        onClick={() => {
          const target = document.getElementById(alpha);
          document
            .querySelectorAll(".selected")
            .forEach((el) => el.classList.remove("selected"));
          target.classList.add("selected");
          window.scrollTo({
            top:
              target.offsetTop -
              parseFloat(
                window
                  .getComputedStyle(document.querySelector("section"))
                  .padding.split(" ")[0]
              ),
          });
        }}
        key={Math.random()}
      >
        {alpha.toUpperCase()}
      </div>
    );
  });
  return (
    <WorkWrapper>
      <ProjectNavigation>{ProjectNavigationItems}</ProjectNavigation>
      <Title>Projects List</Title>
      <ProjectList>{ProjectListItems}</ProjectList>
    </WorkWrapper>
  );
};

네비게이션의 알파벳 클릭시 해당 알파벳을 id 로 가지는 projectlist → element를 찾아 이 태그의 위치 만큼 스크롤을 이동시켜줍니다.

그리고, selected 를 class에 추가 시켜 현재 보고있는 element를 스타일링 해줍니다.

7. 스타일


ProjectNavigation

const ProjectNavigation = styled.div`
  display: flex;
  flex-direction: column;
  position: fixed;
  left: 16px;
  top: 50%;
  transform: translate(0, -50%);
  align-items: center;
  gap: 2px;
  border-radius: 8px;
  padding: 4px;
  background-color: rgba(180, 180, 180, 0.3);
`;

네비게이션의 경우 포지션은 fixed 시켜 두고, 위치를 세로 가운데로 맞춘후 left값으로 살짝 떨어 뜨려줍니다.

ProjectList,Title

const Space = styled.div`
  width: calc(100% - 120px);
  padding: 0 60px;
`;
const Title = styled(Space)`
  font-size: 2rem;
  font-weight: 700;
  margin-bottom: 24px;
`;
const ProjectList = styled(Space)`
  display: flex;
  flex-direction: column;
  gap: 16px;

  .project-list-title {
    font-size: 1.8rme;
    font-weight: 600;
    padding: 4px 8px;
  }
  .project-list-item {
    font-size: 1.6rme;
    font-weight: 400;
    padding: 0 24px 24px 24px;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
  .selected {
    background-color: rgba(150, 150, 150, 0.1);
  }
`;

8. Add Some Func


현재 Project 객체에는 빈 default 값들이 많이 들어 있습니다. 이러한 default 값들은 나중에 프로젝트가 들어갈때마다 삭제 처리 시킬예정입니다. 우선은 등록된 프로젝트들만 보이도록 Project 객체를 filter() 와 reduce() 로 필터링 해 줍니다.

const FilterdProject = Object.keys(Project)
    .filter((k) => !Object.keys(Project[k]).includes("default"))
    .reduce((o, k) => {
      o[k] = Project[k];
      return o;
    }, {});

그리고, Project 가 쓰였던 부분은 FilterdProject를 수정해 주면 됩니다.

9. 전체 코드


// src/pages/work/index.js

// modules
import React, { Fragment, useState, useCallback, useEffect } from "react";
import { Link } from "react-router-dom";

// project
import Project from "@project";
import styled from "styled-components";

const Work = () => {
  const FilterdProject = Object.keys(Project)
    .filter((k) => !Object.keys(Project[k]).includes("default"))
    .reduce((o, k) => {
      o[k] = Project[k];
      return o;
    }, {});
  const ProjectListItems = Object.keys(FilterdProject).map((alpha, i) => {
    let path = `/work/${alpha}`;
    if (Object.keys(Project[alpha]).includes("default")) return null;
    return (
      <div id={alpha} className={i === 0 ? "selected" : ""} key={Math.random()}>
        <div className="project-list-title">
          <span>{alpha.toUpperCase()}</span>
        </div>
        <hr />
        <div className="project-list-item">
          {Object.keys(Project[alpha]).map((key) => {
            return (
              <Link to={`${path}/${key.toLowerCase()}`} key={Math.random()}>
                <span>{key}</span>
              </Link>
            );
          })}
        </div>
      </div>
    );
  });
  const ProjectNavigationItems = Object.keys(FilterdProject).map((alpha) => {
    return (
      <div
        onClick={() => {
          const target = document.getElementById(alpha);
          document
            .querySelectorAll(".selected")
            .forEach((el) => el.classList.remove("selected"));
          target.classList.add("selected");
          window.scrollTo({
            top:
              target.offsetTop -
              parseFloat(
                window
                  .getComputedStyle(document.querySelector("section"))
                  .padding.split(" ")[0]
              ),
          });
        }}
        key={Math.random()}
      >
        {alpha.toUpperCase()}
      </div>
    );
  });
  return (
    <WorkWrapper>
      <ProjectNavigation>{ProjectNavigationItems}</ProjectNavigation>
      <Title>Projects List</Title>
      <ProjectList>{ProjectListItems}</ProjectList>
    </WorkWrapper>
  );
};

export default Work;

const WorkWrapper = styled.div`
  position: relative;
`;
const ProjectNavigation = styled.div`
  display: flex;
  flex-direction: column;
  position: fixed;
  left: 16px;
  top: 50%;
  transform: translate(0, -50%);
  align-items: center;
  gap: 2px;
  border-radius: 8px;
  padding: 4px;
  background-color: rgba(180, 180, 180, 0.3);
`;
const Space = styled.div`
  width: calc(100% - 120px);
  padding: 0 60px;
`;
const Title = styled(Space)`
  font-size: 2rem;
  font-weight: 700;
  margin-bottom: 24px;
`;
const ProjectList = styled(Space)`
  display: flex;
  flex-direction: column;
  gap: 16px;

  .project-list-title {
    font-size: 1.8rme;
    font-weight: 600;
    padding: 4px 8px;
  }
  .project-list-item {
    font-size: 1.6rme;
    font-weight: 400;
    padding: 0 24px 24px 24px;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
  .selected {
    background-color: rgba(150, 150, 150, 0.1);
  }
`;

About 프로젝트는 겹치기 때문에 우석은 삭제 처리하고, 다시 default 를 선언해 주겠습니다.

What is Next?


Work 페이지도 어느정도 된것같습니다.

다음은 About 페이지에 들어갈 skillset 부분의 원형 게이지 부분을 담당하게된 컴포넌트를 svg 프로젝트를 만들어 보겠습니다.

하하하.

끝!!!

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함
반응형