유투브에서 우연히 velopert님의 좋아요 기능 구현 라이브를 보게 되었는데 앱 페이지 기획부터 시작해서 피그마로 직접 디자인하시는 것을 보고 나도 무작정 시작하지 말고 피그마로 디자인부터 짜야겠다고 생각을 했다.
하지만...바로바로 아이디어가 떠오르지 않았다(디자인 생각보다 어렵다고 생각하게 됨).
<극초기 디자인>
설명을 해보자면 왼쪽은 고정되어있는 '사이드바'이고 스크롤을 내리거나 올려도 나의 깃헙, 블로그, 그리고 연락방식을 항상 접근할 수 있도록 만든 것이다.
그리고 오른쪽에 모여있는 스택들은 왼쪽 사이드바에 들어가는 아이콘 모으다가 스택들도 아이콘이 필요하니까 미리 가져오려고 일단 모아두었었다.
그러다가...직접 만들면서 구상을 해야 아이디어가 떠오를 것 같아서 디자인을 하다말고 중간에 바로 코드를 치기 시작했다.
JS 라이브러리
React 라이브러리
CSS in JS
아이콘 라이브러리
애니매이션 라이브러리
처음에는 위의 극초기 디자인 그대로 화면을 만드려고 했지만 막상 만들어보니 너무 붕 떠있는 느낌이여서 헤더에 고정되어있게 변경하였다.
여기에는 Contact(깃허브, 블로그, 이메일) 아이콘을 제외하고, 포트폴리오 목록도 해당목록으로 이동할 수 있게 만들 것이기 때문에 고정된 헤더바를 만들기로 결정하였다.
그렇게 해서 아래 사진처럼 만들게 되었다.
이 부분을 작업할 때 기억에 남는게 있다면...
아이콘을 svg로 넣을때 코드가 너무 길어서 따로 SVG 컴포넌트를 만들었다.
export const SVG = (props) => { return ( <svg width={props.size ? props.size : '2rem'} height={props.size ? props.size : '2rem'} role='img' viewBox='0 0 24 24' fill={props.color} <title>{props.name}</title> <path d={svg[props.name]} /> </svg> ); };
이렇게 틀을 만든 뒤, 아이콘을 사용할 다른 컴포넌트에서 <SVG />
에 props로 전달할 값들을 넣어 재사용이 가능하게 만들었다. svg 속성으로 들어가는 path부분은 SVG컴포넌트 안에 객체로 만들어서 사용하면 된다.
블로그: SVG 컴포넌트 만들기
화면 사이즈를 줄이거나, 모바일로 봤을 때는 헤더 배치를 어떻게 할 것인지 고민이 많이 되었다.
우선, 나의 이름이 화면에서 계속 보였으면 좋겠어서 로고를 하나 새로 만들었고, 포트폴리오 목록은 오른쪽에, 그리고 컨택트 아이콘들은 헤더 우하단에 고정시켜놓았다.
화면을 줄여도 모든 목록과 컨텍트 아이콘들이 나왔으면 좋겠어서 일단 웹 버전은 이렇게 만들었다. 앱 버전은 화면 너비가 더 좁아지기 때문에 추후에 다시 만들 예정이다.
위 디자인이 최종적으로 만들어진 화면이다.
당장은 한국에 있는 회사에 지원할 예정이기 때문에 목록도 영어가 아닌 한국어로 쓰는게 적절한 것 같아 바꾸었다.
목록 부분을 클릭하면 해당 목록 부분으로 가도록 리액트 라이브러리인 react-router-hash-link를 사용하였다. react-router-dom에 있는 Link로는 페이지간 이동만 가능하고, 목록으로는 이동을 할 수가 없기 때문에 hash-link를 사용하였다.
자기소개 부분은 한 문장으로 간결하게 작성하였다. 더 자세한 소개는 다른 페이지로 들어가서 볼 수 있도록 로고 부분과 이미지 우하단에 위치한 +버튼을 클릭하면 나에 대해 더 자세히 알 수 있는 페이지로 이동하게 구현하였다.
자기소개 부분에 애니매이션이 많아서 재밌었던 부분이 많았는데, 몇가지로 추려보자면...
기술 스택 부분에서는 그냥 기술 아이콘만 띄울지, 내가 이 기술을 어디까지 써봤는지 상세 내용도 적어볼지 고민을 해봤는데 그냥 아이콘만 띡 띄우면 내 실력이 어디까지인지 알 수가 없을 것이기 때문에 상세내용도 적자고 결단을 내렸다.
상세내용이 있기 때문에 자리를 꽤 차지해서 정렬을 어떻게 할지 고민하다가 최종적으로는 한줄에 4개씩 배치하기로 했다.
각 기술 아이콘의 박스에 상세내용을 적고, 이를 클릭하면 전체화면으로 펼칠 예정이다. 전체화면으로 펼치는 이유는 더 많은 상세내용을 볼 수 있도록 하기 위함이다.
현재 화면을 보면 우하단에 조그만한 삼각형 버튼이 있다. 이 부분은 맨 위로 가는 버튼이다.
화면 높이가 500까지 내려가면 이 버튼이 아래에서 위로 귀엽게 튀어나온다.(framer-motion 사용) 여기에 걸어놓은 이벤트는 scroll 이벤트인데, 스크롤 할때마다 이벤트가 발생하기 때문에 debounce를 적용할 예정이다.
이 버튼을 누르면 화면의 최상단으로 이동하게 된다. (정확히는 자기소개 부분으로 이동한다.)
처음에는 DOM을 이용해서 scrollTop이 0이 되게 하였는데 hash-link를 사용하는게 더 부드러워 보여서 이로 변경하였다.
+(2022. 08. 25 업데이트)
프로젝트 부분에는 프로젝트 규모에 따라 필터링해서 볼 수 있도록 만들어보았다.
초기화면에서는 아래 사진처럼 전체보기가 눌려져 있는 것처럼 보이고, 프로젝트도 다 나열되어 있다.
만약 사이드 프로젝트만 보고싶거나 미니 프로젝트만 보고 싶다면 그에 해당하는 카테고리 버튼을 누르면 그 카테고리에 해당하는 프로젝트만 볼 수 있게 코드를 짜보았다!
카테고리 버튼 색상 바뀌는 것과 카테고리에 맞게 렌더링 되는 것은 useReducer를 사용하여 카테고리를 클릭할 때마다 state가 바뀌도록 해주었다.
// 프로젝트 카드 ui <ProjectCategoryAll> <span style={{ backgroundColor: state.btnColor.all, color: state.fontColor.all, fontWeight: state.fontBold.all, }} onClick={() => { dispatch({ type: 'ALL' }); }} 전체보기 </span> <span style={{ backgroundColor: state.btnColor.side, color: state.fontColor.side, fontWeight: state.fontBold.side, }} onClick={() => { dispatch({ type: 'SIDE' }); }} 사이드 프로젝트 </span> <span style={{ backgroundColor: state.btnColor.mini, color: state.fontColor.mini, fontWeight: state.fontBold.mini, }} onClick={() => { dispatch({ type: 'MINI' }); }} 미니 프로젝트 </span> </ProjectCategoryAll>
reducer에서는 버튼의 배경 색상
, 버튼 텍스트 색상
, 텍스트 굵기
, 그리고 category
를 변경해주는 일을 한다. 초기 화면에서는 모든 프로젝트가 나와야 하기 때문에 초기 상태값은 '전체보기'를 눌렀을 때와 똑같은 상태가 된다. 그리고 다른 버튼이 눌렸을 때 전체보기에 적용되었던 값은 눌려진 버튼에 적용되어야 한다.
프로젝트 카드의 ui는 똑같아서 프로젝트의 정보를 객체로 된 static data로 만들었고, 이 data를 map으로 돌려서 컴포넌트를 반복해주었다.
처음엔 어떻게 코드를 짜야할까 고민을 많이 했다.
'전체보기' 버튼을 누르면 프로젝트 카드가 전부 렌더링 되어야 하는데, data.category 객체를 전부 배열로 만들고 '전체보기' 문자열을 넣으면 나중에 진행중인 프로젝트가 많아졌을때 전부 다 '전체보기'를 넣어야 하니 좋지 않을 것이다.
그러다 떠오른 최후의 수단은 전체보기의 카테고리는 배열로 ['사이드 프로젝트', '미니 프로젝트']
이렇게 주고 '사이드 프로젝트'나 '미니 프로젝트'는 문자열로 주면 렌더링 할 때 '이 state.category에 data.category가 포함되어 있는가?'로 필터링 할 수 있다. 포함 되어있다면 렌더링, 없다면 렌더링 하지 않게 includes
메서드를 사용해 주었다.
'전체보기'가 눌려진 상태로 초기화 해준다.
// ProjectReducer.js (initArg) export const initCategory = { btnColor: { all: palette.greenColor, side: palette.deeperWhite, mini: palette.deeperWhite, }, fontColor: { all: palette.bgColor, side: palette.fontColor, mini: palette.fontColor, }, fontBold: { all: '700', side: null, mini: null, }, category: ['사이드 프로젝트', '미니 프로젝트'], };
action type이 'SIDE'일 때 사이드 프로젝트의 버튼이 눌려져야 하고, 렌더링은 카테고리가 '사이드 프로젝트'인 데이터(프로젝트)만 렌더링 되어야 한다.
// ProjectReducer.js (reducer) export const categoryReducer = (state, action) => { switch (action.type) { case 'SIDE': return { btnColor: { all: palette.deeperWhite, side: palette.greenColor, mini: initCategory.btnColor.mini, }, fontColor: { all: palette.fontColor, side: palette.bgColor, mini: initCategory.fontColor.mini, }, fontBold: { all: null, side: '700', mini: null, }, category: '사이드 프로젝트', /* 여기 있는 category가 ui를 그릴때 접근할 data의 category와 같은지, 또는 포함되어 있는지 확인 한 후 렌더링 함. */ }; ... ... } }
// ProjectData.js (data) export const projectData = [ { id: 1, category: '미니 프로젝트', name: '프로젝트 이름', description: '프로젝트 설명', img: projectImg, imgSize: { width: 240, height: 370, }, }, ... }
// Project.js (jsx) -> ui 그리는 코드 export const Project = () => { return ( {data.map( (d) => state.category.includes(d.category) && ( <EachProject key={d.id}> // 프로젝트 이미지 <ProjectImgBox> <ProjectImg src='https://via.placeholder.com/240x370' alt='프로젝트 이미지' width={d.imgSize.width} height={d.imgSize.height} /> </ProjectImgBox> <ProjectDesc> // 프로젝트 카테고리 <ProjectCategory> <span>{d.category}</span> </ProjectCategory> // 프로젝트 이름 <ProjectName> <span> {d.name} </span> </ProjectName> // 프로젝트 상세설명 <Projectintro> <span> {d.description} </span> </Projectintro> </ProjectDesc> </EachProject> ) )} ) }
// +(2022. 08. 25 업데이트)