이 글을 지난 10월 9일부터 10월 27일까지 진행된 미니 프로젝트의 과정을 아카이빙하기 위해 작성되었습니다.
지난 프로젝트와 마찬가지로 데일리 스탠드업 미팅(Daily Standup Meeting)
을 매일 진행했습니다.
이슈 트랙커로는 트렐로(Trello)
를 사용했습니다.
페어 프로그래밍(pair programming)
을 시도해봤던 프로젝트입니다. 다만, 기존 3인의 프론트엔드 개발자 체제에서 1인이 이탈함에 따라 인력 부족으로 더 이상 진행되지 못했습니다.
이번 프로젝트에서는 프로덕트 매니저(Product Manager)
역할을 수행하여 프로젝트 전반에서 의사 결정에 큰 목소리를 냈습니다. 그리고 3주 간의 개발 기간 후 진행되는 데모데이
에서 팀을 대표해 발표를 맡게 되었습니다. 발표 자료는 아래에서 확인하실 수 있습니다.
컴포넌트 주도 개발(Component Driven Development)
로 프로젝트 기반을 구축했습니다.Modal Popup
은 웹 개발에서 빠질 수 없는 컴포넌트입니다. 따라서 이 글에서는 이 컴포넌트에 대해 언급하고자 합니다.// Modal.js
function Modal({ data, onClose, ...props }) {
return (
<ModalPopup>
<Backdrop onClick={onClose} />
<Content {...props}>
<ModalCloseButton
type="button"
aria-label="모달 팝업 닫기"
onClick={onClose}
>
<MoreIcon />
</ModalCloseButton>
{data}
</Content>
</ModalPopup>
);
}
data
입니다. 실제 Modal 안의 컨텐츠를 의미하며, 각 컨텐츠를 컴포넌트 파일로 만들어 주입하는 방식을 선택했습니다. 아래가 그 예시입니다.<CartButton onClick={modalHandler} />
<Portal>
{modalOpen && (
<Modal
data={
<Purchase
productId={productId}
productName={productName}
totalPrice={totalPrice}
onClose={modalHandler}
/>
}
onClose={modalHandler}
/>
)}
</Portal>
onClose
는 이 Modal 컴포넌트를 닫기 위한 함수가 담긴 props입니다. Modal 컴포넌트에서는 Modal 영역이 아닌 영역을 클릭해도, 닫기 버튼을 클릭해도 닫혀야 하므로, 아래 컴포넌트에 주입합니다.z-index
레벨에 따라 z축으로의 위치가 결정되는만큼, Modal은 root와 동격으로 존재해야 합니다. 이를 위해 Portal
을 사용해야 합니다.index.html
로 접근합니다.// index.html
<div id="root"></div>
<div id="modal"></div>
// Portal.js
import reactDom from 'react-dom';
const Portal = ({ children }) => {
const el = document.getElementById('modal');
return reactDom.createPortal(children, el);
};
export default Portal;
createPortal 메서드
로 DOM 계층 최상위에 렌더링 준비를 마칩니다.styled components
는 CSS를 컴포넌트화하는 라이브러리입니다. styled components
는 HTML + CSS + JavaScript라는 방식으로 셋을 묶어 JS 파일 하나에서 컴포넌트 단위로 개발
할 수 있게 합니다.// Button.js
import styled from 'styled-components';
const Button = ({
type = 'button',
content = 'button',
onClick,
disabled = false,
...props
}) => {
return (
<DefaultButton
type={type}
aria-label={content}
onClick={onClick}
disabled={disabled}
{...props}
>
{content}
</DefaultButton>
);
};
/**
* Button props list
* @property {string} type: button, submit, reset - 버튼 타입을 정의합니다.
* @property {string} shape: solid, outline - 버튼 형태를 정의합니다.
* @property {string} color: primary, secondary, neutral - 버튼 색상을 정의합니다.
* @property {string} size: small, medium, large - 버튼 크기를 정의합니다.
* @property {string} content - 버튼 내부 텍스트와 웹 접근성 처리에 사용합니다.
* @property {function} onClick - 버튼 클릭 시 실행할 함수를 위해 미리 정의합니다.
* @property {boolean} disabled - 버튼의 비활성화 상태를 정의합니다.
*/
const SIZE_STYLES = {
small: {
padding: '11px 10px',
fontSize: '16px',
},
medium: {
padding: '12px 10px',
fontSize: '20px',
},
large: {
padding: '13px 10px',
fontSize: '24px',
},
};
const DefaultButton = styled.button`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
border: 1px transparent solid;
border-radius: 4px;
opacity: 0.9;
cursor: pointer;
padding: ${({ size }) => SIZE_STYLES[size]?.padding || '13px 10px'};
font-size: ${({ size }) => SIZE_STYLES[size]?.fontSize || '20px'};
border-color: ${props =>
(props.color === 'primary' && props.theme.primaryColor) ||
(props.color === 'secondary' && props.theme.secondaryColor) ||
props.theme.grayscaleD};
&:hover,
&:active {
opacity: 1;
}
&[disabled] {
opacity: 0.2;
cursor: not-allowed;
}
${props => {
if (props.shape === 'solid') {
return `
background-color: ${
(props.color === 'primary' && props.theme.primaryColor) ||
(props.color === 'secondary' && props.theme.secondaryColor) ||
props.theme.grayscaleD
};
color: ${props.theme.grayscaleA};
`;
} else {
return `
background-color: ${props.theme.grayscaleB};
color: ${props.theme.grayscaleC};
`;
}
}}
`;
export default Button;
styled components
를 import하고, Button 컴포넌트의 필수적인 props를 가려냅니다. type
, content
, onClick
, disabled
까지 추린 이후에 형태나 색상, 크기와 같은 디자인적 부분은 spread operator props
로 전달받도록 합니다.styled components
의 syntax는 그리 어렵지 않습니다. 사용자 정의 태그를 변수로 지정하고, styled.tag라는 문법이 뒤따르며, CSS properties와 values에서 props를 통해 스타일링을 하든가, 분기를 하는 식으로 처리하는 것이 전부입니다.Theme Provider
로 테마 전환 기능을 구현했습니다.// theme.js
export const lightTheme = {
primaryColor: '#a29bfe',
secondaryColor: '#e71d36',
grayscaleA: '#fff',
grayscaleB: '#efefef',
grayscaleC: '#dfdfdf',
grayscaleD: '#c8c8c8',
grayscaleE: '#b7b7b7',
grayscaleF: '#000',
};
export const darkTheme = {
primaryColor: '#a29bfe',
secondaryColor: '#e71d36',
grayscaleA: '#000',
grayscaleB: '#b7b7b7',
grayscaleC: '#c8c8c8',
grayscaleD: '#dfdfdf',
grayscaleE: '#efefef',
grayscaleF: '#fff',
};
export const theme = {
lightTheme,
darkTheme,
};
Theme Provider
와 테마 스위치 기능
을 적용합니다.// Root.js
import React, { useState } from 'react';
import { ThemeProvider } from 'styled-components';
import { lightTheme, darkTheme } from './styles/theme';
import ThemeSwitcher from './modules/themeSwitcher';
import GlobalStyle from './styles/GlobalStyle';
const Root = () => {
const [isLightTheme, setIsLightTheme] = useState(true);
const switchTheme = () => {
setIsLightTheme(prev => !prev);
};
return (
<ThemeProvider theme={isLightTheme ? lightTheme : darkTheme}>
<GlobalStyle />
<Router />
<ThemeSwitcher
switchTheme={switchTheme}
isLightTheme={isLightTheme}
/>
</ThemeProvider>
);
}
export default Root;
body {
background-color : ${props => props.theme.grayscaleA};
color : ${props => props.theme.grayscaleF};
}
state 끌어올리기
, 즉 Lifting State Up
으로 Router.js부터 Detail.js까지 함수를 끌고 다니면서 호출했습니다. 그리고 이러한 불필요한 행위가 반복됨에 따라 전역 상태 관리
라는 것에 대한 갈증이 심해졌습니다.store
, reducer
, action
, dispatch
, useSelector hook
이 바로 그것들입니다.store
는 상태가 저장되는 공간입니다. createStore 메서드
로 생성하고 상태를 꺼내쓸 수 있게 해줍니다.reducer
함수는 변화를 일으키는 함수로 상태(state)와 액션(action) 파라미터를 받습니다. 그리고 상태와 액션을 참조하여 새로운 상태를 반환합니다.action
은 상태 변화를 위한 것으로, 미리 등록하고 type을 적습니다.store
의 내장 함수인 dispatch
를 통해 action
은 물론이고, 데이터까지 보낼 수 있습니다. 데이터는 payload 안에 담아야 합니다. dispatch
를 사용하려면 useDispatch hook
이 필요합니다.useSelector hook
으로 store
에 저장된 상태를 꺼낼 수 있습니다.// Root.js
import React, { useState } from 'react';
import Router from './Router';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import { lightTheme, darkTheme } from './styles/theme';
import ThemeSwitcher from './modules/themeSwitcher';
import GlobalStyle from './styles/GlobalStyle';
const Root = () => {
// [Redux] 초기값을 빈 배열로 설정합니다. 여러 객체가 들어가야 하므로 배열 타입이 적절합니다.
let initialValue = [];
// [Redux] reducer 함수는 변화를 일으키는 함수로 상태(state)와 상태 변화를 위한 액션(action) 파라미터를 받습니다. 그리고 상태와 액션을 참조하여 새로운 상태를 반환합니다.
function reducer(state = initialValue, action) {
// 아래 두 콘솔은 풀어서 직접 확인하시는 것을 추천합니다.
// console.log(action);
// console.log(action.payload);
// [Redux] initialValue는 불변성(Immutability)을 가져야 하므로, 이것을 복사할 변수가 필요합니다. 여기서는 직관적으로 copyValue라 하겠습니다. spread operator로 초기 상태값을 가져옵니다.
let copyValue = [...state];
// [Redux] 액션(action)을 등록하고, type을 적습니다. type은 대문자로 적는 것이 컨벤션이라고 합니다.
if (action.type === 'ADD') {
// [Redux] ADD 액션이 실행되면 copyValue에 payload를 push합니다. payload 안에는 여러 데이터를 담을 수 있습니다.
copyValue.push(action.payload);
return copyValue;
} else if (action.type === 'UPDATE') {
copyValue.push(action.payload);
return copyValue;
}
}
// [Redux] 전역 상태 관리 도구인 Redux에서 실제 상태가 저장되는 공간인 store를 생성합니다. 이제는 store에서 데이터를 꺼내 사용할 수 있게 되었습니다.
const store = createStore(reducer);
const [isLightTheme, setIsLightTheme] = useState(true);
const switchTheme = () => {
setIsLightTheme(prev => !prev);
};
return (
// [Redux] 전역 상태이므로 최상위 Provider에 주입합니다.
<Provider store={store}>
<ThemeProvider theme={isLightTheme ? lightTheme : darkTheme}>
<GlobalStyle />
<Router />
<ThemeSwitcher
switchTheme={switchTheme}
isLightTheme={isLightTheme}
/>
</ThemeProvider>
</Provider>
);
};
export default Root;
// Purchase.js
import { useDispatch } from 'react-redux';
const Purchase = ({ productId, productName, totalPrice, onClose }) => {
const dispatch = useDispatch();
const putInCart = () => {
// [Redux] 전역 상태를 변경하는 유일한 방법은 액션을 발생시키는 겁니다. store의 내장 함수인 dispatch를 통해 액션은 물론이고, 데이터까지 보낼 수 있습니다. 데이터는 payload 안에 담아야 합니다
dispatch({
type: 'ADD',
payload: { productId: productId, quantity: quantity },
});
onClose();
};
return (
...
)
};
export default Purchase;
// GnbCartButton.js
import { useSelector } from 'react-redux';
const GnbCartButton = () => {
// [Redux] useSelector hook으로 store에 저장된 데이터(productId, quantity)를 꺼내옵니다.
let state = useSelector(state => {
return state;
});
// store에서 꺼내온 데이터를 배열 순회하면서 총 수량을 구합니다. 그리고 이 수량을 GNB의 장바구니 버튼 옆에 표시합니다.
const sumQuantity = state => {
if (Array.isArray(state)) {
return state.reduce((prev, current) => prev + current.quantity, 0);
}
};
return (
...
)
}
export default GnbCartButton;
부트캠프 wecode에서의 마지막 프로젝트가 종료되었습니다.
Vite로 React 초기 세팅하기
와 infinite scroll
를 제외하면 어설프게나마 구현했습니다. 개인 포트폴리오 작업을 통해 미구현한 것들을 구현하고자 합니다.