BEB 과정의 두 번째 프로젝트였다. 첫 프로젝트보다 더 긴 시간이 주어졌지만 전보다 더 많은 기능들을 구현해야 했기 때문에 훨씬 더 많은 노력을 기울여야 했다.
✏️ 역할 분담
- Front-end (내가 담당한 🙋🏻♀️)
클라이언트 웹사이트- Back-end
서버, DB, web3, demon- Smart Contract
ERC-20, ECT-721 컨트랙트 개발 및 발행
스팀잇, gm, 트위터를 간단하게 합친 형태의 web2 커뮤니티이다. 커뮤니티에 참여하는 유저들의 각 활동에 대해 NGT라는 ERC-20 토큰이 보상으로 부여된다. ERC-20, ERC-721을 이용하지만 사용자는 직접 지갑을 연결할 필요는 없다. 모든 컨트랙트 관련 작업은 서버단에서 처리된다.
유저는 로그인, 포스트/댓글 작성, 작성한 포스트의 좋아요 갯수, 다른 유저의 도네이션 등을 통해 ERC-20으로 보상을 받을 수 있다.
이 기능들은 (당연하지만) 모두 유저 본인의 포스트에 대해서는 실행할 수 없다.
블록체인의 속도 문제로 인해, 보상은 실시간으로 지급되지 않고 운영자가 원하는 때에 일괄 반영된다. admin 계정으로 로그인했을 때만 보이는 보상 지급 버튼이 구현되어 있다. 운영자가 이 버튼을 누르면 지금까지 누적된 모든 행위들에 대한 보상 지급이 이루어진다.
모든 포스트에는 여느 SNS처럼 원하는 태그를 달 수 있다. 그리고 이 태그들은 집계되어 사이트 Home 상단에는 현재 기준 가장 많이 언급된 태그들이 출력된다. 각 태그를 클릭하면 해당 태그를 포함한 포스트들만 필터링한 결과를 볼 수 있다. 또한 헤더의 Search Bar를 이용해 원하는 태그를 포함한 포스트들을 직접 검색할 수도 있다.
NFT와 관련된 작업을 할 수 있는 영역을 독립적으로 배치해두었다. NFT는 일종의 회원 등급을 나타낸다. 등급이 높은 유저일 수록 보상 비율이 커진다. 각 유저는 최초 1회 NFT를 민팅할 수 있으며 민팅을 위해 일정량의 NGT 토큰을 지불해야 한다. 민팅받은 NFT에는 등급 레벨이 담겨져 있다. 등급 레벨은 랜덤하게 부여되며 레벨이 높을 수록 보상 비율이 더 커진다.
이 등급 레벨 NFT는 유저간 거래가 가능하다. 내가 랜덤으로 뽑은 NFT에 높은 레벨이 담겨있다면 높은 금액으로 팔 수 있고, 반대로 더 높은 보상을 위해 다른 유저의 NFT를 구입할 수도 있다.
현재 NFT Zone는 NFT를 발급하는 단계까지만 구현되어 있다.
댓글과 대댓글은 로그인하지 않은 익명의 유저도 남길 수 있다. 익명으로 댓글을 남기기 위해서는 비밀번호를 입력해야 하며, 나중에 댓글을 지우고자 할 때 비밀번호를 입력해야 한다.
Hot Tags Now
Post Creation Form
Posts
스팀잇처럼 컨텐츠를 게시하고 이에 따른 보상을 받을 수 있는 gm과 전체 구성이 비슷하게 구현되어 있다. 포스트는 트위터나 인스타그램처럼 단순화하여서 각 포스트의 상세 페이지는 따로 없고 메인 화면에 모든 내용이 출력된다.
Posts
로그인한 유저 본인이 작성한 포스트들만 따로 확인할 수 있다.
NGT(ERC-20) Balance
Transaction List
(Admin user의 경우) 보상 토큰 지급 버튼
Posts
- react
- redux tool-kit (전역 상태 관리)
- redux persist (새로고침 시에도 로그인 유지)
- react query (서버에 데이트 요청 시)
- MUI
RTK는 리덕스를 좀 더 쉽게 사용하기 위한 툴킷이다. redux를 잘 사용하기 위해서는 immer, reselect, thunk, saga 등의 추가 라이브러리를 설치해야 하는 경우가 많다(고 한다). RTK는 리덕스에서 공식적으로 제공하는 툴킷으로 추가 라이브러리 설치가 필요 없다.
하지만 페이지를 새로고침할 경우 redux store
의 state
가 날아가버리는 것을 해결하기 위해서는 다른 라이브러리가 필요하다. redux persist
를 사용하면 아주 간단하게 이 문제를 해결할 수 있다.
이곳에서 react toolkit
기본 설치 방법을 참고하였고, 이곳에서 redux toolkit
에 redux persist
를 적용하는 방법을 참고했다.
combiendReducer
와 persistConfig
를 선언해주고, 이를 활용해 persistReducer
를 생성 하고 store
를 구성한다.
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './user';
import tagReducer from './tag';
import selectedTagReducer from './selectedTag';
import storage from 'redux-persist/lib/storage';
import { combineReducers } from "redux";
import { persistReducer } from 'redux-persist'
import thunk from 'redux-thunk';
const reducers = combineReducers({
user: userReducer,
tag: tagReducer,
selectedTag: selectedTagReducer,
});
const persistConfig = {
key: 'root',
storage,
};
const persistedReducer = persistReducer(persistConfig, reducers);
const store = configureStore({
reducer: persistedReducer,
devTools: process.env.NODE_ENV !== 'production',
middleware: [thunk]
})
export default store;
react-query
는 client-server 사이의 비동기 로직을 간편하게 다룰 수 있게 해준다. get
요청에는 useQuery
를 post/put/delete
요청에는 useMutation
을 쓰는 것이 기본적인 내용이다. react-query
자체가 통신을 처리하지는 않고 axios
나 fetch
를 감싸는 형태로 구성되며 아래처럼 매우 다양한 반환값을 자체적으로 제공하기 때문에 원하는 대로 적절히 사용할 수 있다.
const {
data,
dataUpdatedAt,
error,
errorUpdatedAt,
failureCount,
isError,
isFetched,
isFetchedAfterMount,
isFetching,
isIdle,
isLoading,
isLoadingError,
isPlaceholderData,
isPreviousData,
isRefetchError,
isRefetching,
isStale,
isSuccess,
refetch,
remove,
status,
} = useQuery(queryKey, queryFn?, {
onError,
onSettled,
onSuccess,
placeholderData,
queryKeyHashFn,
refetchInterval,
refetchIntervalInBackground,
refetchOnMount,
refetchOnReconnect,
refetchOnWindowFocus,
retry,
retryOnMount,
retryDelay,
select,
staleTime,
structuralSharing,
suspense,
useErrorBoundary,
})
비동기 로직 성공시 반환되는 데이터 혹은 실패시 발생하는 에러에 대한 처리를 간단히 처리해줄 수 있고 isFetching
, isLoading
등을 활용하여 데이터가 페칭/로딩되는 동안의 로직까지 간편하게 구성해줄 수 있다.
react-query
는 아직 많이 쓰이고 있지는 않은 듯 하다. 구글링했을 때 여러 자료가 검색되지만, 다양한 사례를 다룬 내용은 잘 없는 것 같다. react-query docs를 참고하는 편이 낫다. docs가 꽤나 친절하게 구성되어 있다. 앞으로 점점 유저가 늘어나지 않을까 생각된다.
아래의 코드는 각 포스트를 불러오기 위한 useQuery
문이다. 메인 화면에서는 지금까지 작성된 모든 포스트들을 한꺼번에 불러와야 한다. 따라서 작성된 포스트의 개수만큼 useQuery
가 실행되는데 이 때 에러가 발생해서 한참을 고생했다. 각 포스트마다 queryKey
를 고유한 값으로 변경해주니 잘 동작했다 😭 페이지나 컴포넌트 안에서 한번만 실행되는 useQuery
문의 경우 상관 없지만 이렇게 반복적으로 데이터를 불러들일 때는 꼭 queryKey
를 unique하게 만들어줘야 한다!
// 🔥 useQuery Key.... unique하게 작성!!!
const {data, status} = useQuery(`getPost_${uuid}`, () => {
return axios.get(`/api/post/getPost?postUuid=${uuid}`)
.then((res) => {
return res.data.data.post;
})
})
새로운 댓글을 작성하는 useMutation
구문이다. 코드가 매우 직관적으로 구성됨을 알 수 있다.
const newCommentMutation = useMutation(((comment) => {
return axios.post('/api/comment/sendMemberComment', comment, {
headers: {
"Authorization": `bearer ${accessToken}`
}
})
}), {
onSuccess: () => {
alert('😄 The comment has been created successfully');
commentRef.current.value = '';
},
onError: (error) => {
alert(`
❗️ Something Wrong! Please try again
(${error})
`);
}
});
이렇게 선언된 useMutation은, 원하는 곳에서 아래와 같이 실행시킬 수 있다.
newCommentMutation.mutate(comment);
또한 useMutation
이 반환하는 data
를 아래와 같이 활용하면, 리액트 컴포넌트의 리턴문 안에서 바로 사용이 가능하기 때문에 매우 편리하다.
const HotTags = (props) => {
const {data} = useQuery('getHotTags', () => {
return axios.get('/api/hashtag/topHashtag')
.then((res) => {
return res.data.data.hashTag;
})
})
return (
<Card sx={{mb: 2, p: 2}}>
<Typography variant="body2" color="primary" fontSize={'28px'}>
🔥 Hot Tags Now!
</Typography>
{
data &&
data.map((tag, idx) => <Tag color="primary" keyword={tag.tag} key={idx} />)
}
</Card>
)
}
지난 프로젝트에서는 순수 CSS만을 활용했었는데, 이번에는 MUI를 처음으로 사용해보았다. 사용해보고 느낀 것이 CSS 툴이나 템플릿은 정말 편리하고 빠르게 내가 원하는 UI를 만들어낼 수 있지만 편리한 만큼 자유도가 떨어진다는 점이다. 미세한 수정을 위해 필요 이상의 에너지를 써야할 때가 있다. 처음엔 아예 미리 개발되어 있는 템플릿을 그대로 가져다가 쓰려다가 이런 자유도 문제에 답답함을 크게 느껴 MUI로 넘어갔다. MUI는 작은 기능 단위의 모듈들을 제공하기 때문에 디자인 수정에 대한 자유도가 그래도 비교적 높은 편이다.
또한 이곳에서 custom theme를 직접 만들어 적용할 수 있었다. 원하는 색상 조합과 사용하고자 하는 폰트 몇가지만 구성해주면 알아서 테마 코드가 만들어지고 이를 app.js
에 넣어주면 사이트 전체에 반영된다.
const customTheme = createTheme({
palette: {
type: 'dark',
primary: {
main: '#dba531',
},
secondary: {
main: '#007849',
},
error: {
main: '#ff7605',
},
success: {
main: '#f7b92a',
},
background: {
default: '#292b33',
paper: '#3a3a3f',
},
info: {
main: '#00c4b5',
},
},
typography: {
fontFamily: 'Montserrat',
fontSize: 14,
fontWeightRegular: 400,
fontWeightMedium: 600,
fontWeightLight: 200,
fontWeightBold: 700,
h1: {
fontWeight: 400,
fontFamily: 'Permanent Marker',
fontSize: '2.9rem',
},
},
});
function App() {
return (
<ThemeProvider theme={customTheme}>
// ...
</ThemeProvider>
);
}
다양한 툴의 활용
개발자는 깊게가 아니라 넓게 알아야 한다는 말을 실감했다. 물론 react와 기본 css 만으로도 비슷한 결과물을 만들어낼 수 있을 것이다. 하지만 훨씬 오랜 시간이 걸렸겠지.. 모든 툴과 기술 스택의 사용법을 알 필요는 없지만 '이럴 때 활용할 수 있는 이런 툴이 있다~'는 정도는 알고 있어야 효율적인 개발을 할 수 있을 것이다. 나는 사실 리덕스나 MUI 정도만 적용해볼 생각이었는데, 팀원분들이 이런 것들이 있다고 알려주셔서 다양한 시도를 해볼 수 있었다.
frontEnd - backEnd 간의 워크플로우
프론트엔드와 백엔드는 API로 소통을 하기 때문에 깔끔하게 의사 전달 및 요청이 가능한 것 같다. API 문서가 잘 짜여져 있어야 한다고 배워왔는데 어떤 의미인지 알 수 있었다. 우리 사이트에는 매우 다양한 기능들이 적용되어 있는데 이런 내용을 그냥 말로 소통했다면 아마 너무 어려웠을 것 같다. 백엔드 담당자가 개발한 내용을 API로 깔끔하게 정리해서 보내주니 프론트 입장에서도 개발하기가 훨씬 수월했다.
bare minimum 과제로 정해둔 기능까지는 모두 구현했다. 하지만 advanced까지 진행했다면 더 재밌었을텐데.. 하는 짧은 기간에 대한 아쉬움이 있었다. 프론트 개발에서 가장 아쉬웠던 부분은 무한 스크롤을 구현하지 못한 것과, 이미지 로딩 인디케이터 구현에 실패한 것이다.
memo
, useCallback
등을 통한 성능 최적화를 하지 않음components
폴더에 28개의 컴포넌트를 냅다 때려박아 두었다. 서비스 로직도 구분하지 못했고 그야말로 구조 관리가 엉망이다. 코드를 짠 내가 아니면 원하는 코드를 찾아서 읽기 어려울 것 같다 😅 신경써야겠다 싶었을 때에는 이미 손쓰기 어려워서 그냥 반성만 하고 '다음엔 잘해야지...'로 정리했다.state
를 활용한 인디케이터를 추가하는 등의 다양한 방법을 시도해보았지만 제대로 작동하는 방법이 하나도 없었다 😭 여기에 시간을 정말 많이 들인 것 같은데 대체 왜 안되는 걸까?!! 임시방편으로 그냥 이미지를 선택한 직후 로딩스피너를 약 4초간 돌아가게 해두었는데 사실 눈속임과 같은 방법이라 개선이 꼭 필요하다. 사실 이미지 첨부가 완료되면 파일의 이름을 출력시키는 등의 방법도 있지만, 이 경우에도 혹시 유저가 파일 이름이 뜨기 전에 submit을 눌러버린다면? 결국 같은 문제가 발생하기 때문에 '이미지 파일이 잘 첨부되고 있는 중이다~'를 나타내는 방법이어야 할 것 같다.프로젝트 1,2에서 연달아 프론트엔드를 담당했다. 내가 프론트를 희망했기 때문에 즐겁게 참여했고 프론트에 대해서는 정말 많이 배울 수 있는 기회였지만, 백엔드와 컨트랙트에는 참여하지 못해 아쉬움이 크다. 아마 프로젝트3에서는 프론트/web3/DB/컨트랙트 모두를 조금씩은 다뤄볼 수 있을 것 같아서 기대가 된다.