완성 결과물
결과물 GIF
1.기본 & 커스텀 모드를 선택할 수 있다.
2. 커스텀 모드일 경우 1번 색상과 2번 색상을 지정해 그라이데션을 설정할 수 있다.
3. 색상 설청창 외부 클릭시 닫힌다.
설계 및 주요 함수
위 결과물은 별도 라이브러리를 사용하지 않았다.
input 태그에서 type="color"로 지정하면 색상 그라데이션을 설정할 수 있다.
<BtnColor1>
{defaultSetting ? null : (
<input
id="style1"
type="color"
defaultValue={color1}
onChange={(e) => setColor1(e.target.value)}
/>
)}
</BtnColor1>
input 태그는 색상을 HEX 코드로만 인식할 수 있기 때문에 아래 단계를 거쳤다.
1) 2가지 색상을 HEX 코드 입력
2) 2가지 색상을 RGB 코드로 변환 후 중간 값들을 계산
3) 결과 값들을 HEX 코드로 반환
RGB → HEX 변환 함수
const rgbToHex = (r, g, b) => {
r = r.toString(16);
g = g.toString(16);
b = b.toString(16);
if (r.length === 1) r = "0" + r;
if (g.length === 1) g = "0" + g;
if (b.length === 1) b = "0" + b;
return "#" + r + g + b;
};
HEX → RGB 변환 함수
const hexToRgb = (hex, alpha) => {
let r = parseInt(hex.slice(1, 3), 16),
g = parseInt(hex.slice(3, 5), 16),
b = parseInt(hex.slice(5, 7), 16);
/* Original Code */
// if (0 <= alpha && alpha <= 1) {
// return `rgba(${r}, ${g}, ${b}, ${alpha})`;
// } else {
// return `rgb(${r}, ${g}, ${b})`;
// }
if (0 <= alpha && alpha <= 1) {
return [r, g, b, alpha];
} else {
return [r, g, b];
}
};
Generate Gradient Color 함수
※ n = 생성할 컬러칩 개수
1, 2번째 선택한 색상(HEX)를 RGB로 변환, 중간 값들을 n개의 수만큼 비율로 계산 후 Return한다.
const gradientPalette = (color1, color2, n) => {
// HEX 코드를 → RGB 변환
const rgbColor1 = hexToRgb(color1);
const rgbColor2 = hexToRgb(color2);
// 1번째로 선택한 색상 추가
const result = [color1];
// 1번과 2번 색상의 RGB값을 n개만큼 비율로 나누어 계산하여 추가
for (let i = 1; i < n - 1; i++) {
const midValue = [];
for (let j = 0; j < 3; j++) {
if (rgbColor1[j] > rgbColor2[j]) {
midValue.push(
rgbColor1[j] -
parseInt(Math.abs(rgbColor1[j] - rgbColor2[j]) / (n - 1)) * i
);
} else if (rgbColor1[j] === rgbColor2[j]) {
midValue.push(rgbColor1[j]);
} else if (rgbColor1[j] < rgbColor2[j]) {
midValue.push(
rgbColor1[j] +
parseInt(Math.abs(rgbColor1[j] - rgbColor2[j]) / (n - 1)) * i
);
}
}
result.push(rgbToHex(midValue[0], midValue[1], midValue[2]));
}
// 마지막으로 끝에 2번째로 선택한 색상 추가
result.push(color2);
return result;
};
외부 DOM 클릭시 닫힘
색상설정 창 이외의 dom을 클릭했을 경우 색상설정창이 닫힌다.
useEffect(() => {
setColorPalette(gradientPalette(color1, color2, colorChipCount));
// NOTE Dropdwon 박스 바깥쪽을 클릭시 옵션이 사라지는 기능
function handleClickOutside(event) {
if (
colorPickRef.current &&
!colorPickRef.current.contains(event.target)
) {
// dom 바깥 클릭시 닫힘 상태 설정
setTimeout(function () {
setColorPickOpen(false);
}, 100);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [color1, color2, colorPickRef]);
※ setTimeout을 사용한 이유
dom 바깥(노란색 영역을 제외한 영역)을 클릭하면 false를 설정했는데 "색상설정" 버튼을 클릭시 onClick보다 먼저 동작하여 colorPickOpen이 false로 전환, 이어서 onClick 함수가 작동하며 다시 True로 상태가 색상설정 창이 무한으로 켜져있는 현상이 발생하여 이를 해결하고자 setTimeout을 사용했다.
<BtnButton type="button" onClick={() => setColorPickOpen(true)}>
전체 코드
import React, { useEffect, useState, useRef } from "react";
import styled from "styled-components";
const BtnButton = styled.button`
position: relative;
`;
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
width: 300px;
height: 200px;
align-items: center;
border: 1px solid #11233f;
border-radius: 10px;
position: absolute;
background-color: #ffffff;
font-size: 14px;
.flexColumnCenter {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
`;
const Title = styled.div`
width: 100%;
height: 35px;
background-color: #11233f;
color: #ffffff;
border-radius: 10px 10px 0px 0px;
`;
const OptionContainer = styled.div`
width: 100%;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
margin: 10px;
gap: 20px;
.active {
background-color: #11233f;
background: #11233f;
color: #eeeeee;
}
.nonActive {
background-color: #e5e5e5;
}
`;
const BtnOption = styled.div`
width: 90px;
height: 35px;
border-radius: 5px;
cursor: pointer;
`;
const ColorContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 50px;
input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: transparent;
width: 35px;
height: 36px;
border: none;
}
// -webkit- : 크롬, 사파리 , 오페라
// -moz- : firefox
input::-webkit-color-swatch {
border-radius: 50%;
border: none;
}
input::-moz-color-swatch {
border-radius: 50%;
border: none;
}
`;
const BtnColor1 = styled.div`
input {
cursor: pointer;
}
`;
const BtnColor2 = styled.div`
input {
cursor: pointer;
}
`;
const ColorChip = styled.div`
width: 30px;
height: 30px;
`;
const BtnSubmit = styled.div`
width: 50px;
height: 35px;
color: #ffffff;
font-size: 14px;
background-color: #11233f;
border-radius: 10px;
cursor: pointer;
`;
/* NOTE - 원리
input은 hex 코드만 인식할 수 있다.
hex코드를 입력 받으면 rgb로 변환 후 useState에 저장
→ 선택된 2개의 색상을 gradientPalette 함수에 넣는다.
→ 함수에서 중간 컬러 RGB 값들을 계산
→ 계산된 중간 값들을 useState colorPalette에 리스트로 저장하고 순차적으로 표출
*/
// RGB → HEX 함수
const rgbToHex = (r, g, b) => {
r = r.toString(16);
g = g.toString(16);
b = b.toString(16);
if (r.length === 1) r = "0" + r;
if (g.length === 1) g = "0" + g;
if (b.length === 1) b = "0" + b;
return "#" + r + g + b;
};
// HEX → RGB 함수
const hexToRgb = (hex, alpha) => {
let r = parseInt(hex.slice(1, 3), 16),
g = parseInt(hex.slice(3, 5), 16),
b = parseInt(hex.slice(5, 7), 16);
/* Original Code */
// if (0 <= alpha && alpha <= 1) {
// return `rgba(${r}, ${g}, ${b}, ${alpha})`;
// } else {
// return `rgb(${r}, ${g}, ${b})`;
// }
if (0 <= alpha && alpha <= 1) {
return [r, g, b, alpha];
} else {
return [r, g, b];
}
};
// Generate Gradient Color 함수 (n = 생성할 컬러칩 개수)
// 과정 : HEX 코드를 입력 → RGB값으로 연산 → HEX 코드 return
const gradientPalette = (color1, color2, n) => {
// HEX 코드를 → RGB 변환
const rgbColor1 = hexToRgb(color1);
const rgbColor2 = hexToRgb(color2);
// 1번 선택한 색상 추가
const result = [color1];
// 1번과 2번 색상의 RGB값을 n개만큼 비율로 나누어 계산하여 추가
for (let i = 1; i < n - 1; i++) {
const midValue = [];
for (let j = 0; j < 3; j++) {
if (rgbColor1[j] > rgbColor2[j]) {
midValue.push(
rgbColor1[j] -
parseInt(Math.abs(rgbColor1[j] - rgbColor2[j]) / (n - 1)) * i
);
} else if (rgbColor1[j] === rgbColor2[j]) {
midValue.push(rgbColor1[j]);
} else if (rgbColor1[j] < rgbColor2[j]) {
midValue.push(
rgbColor1[j] +
parseInt(Math.abs(rgbColor1[j] - rgbColor2[j]) / (n - 1)) * i
);
}
}
result.push(rgbToHex(midValue[0], midValue[1], midValue[2]));
}
// 마지막으로 끝에 2번 선택한 색상 추가
result.push(color2);
return result;
};
export const ColorPicker = () => {
const [defaultSetting, setDefaultSetting] = useState(true); // #c2c2c2
const [colorPickOpen, setColorPickOpen] = useState(false);
const [color1, setColor1] = useState("#f0f8ff"); // #c2c2c2
const [color2, setColor2] = useState("#4f76d9"); // #c2c2c2
const [defaultColorPalette, setDefaultColorPalette] = useState([]);
const [colorPalette, setColorPalette] = useState([]);
const colorPickRef = useRef();
// 초기값
const defaultColor1 = "#f0f8ff";
const defaultColor2 = "#4f76d9";
const colorChipCount = 7;
const handleDefault = () => {
setDefaultSetting(true);
};
const handleCustom = () => {
setColorPalette(gradientPalette(color1, color2, colorChipCount));
setDefaultSetting(false);
};
const handleSaveColor = () => {
setColorPickOpen(false);
};
// 초기 랜더링시 defaultColorPalette 색상 저장
useEffect(() => {
setDefaultColorPalette(
gradientPalette(defaultColor1, defaultColor2, colorChipCount)
);
}, []);
useEffect(() => {
setColorPalette(gradientPalette(color1, color2, colorChipCount));
// NOTE Dropdwon 박스 바깥쪽을 클릭시 옵션이 사라지는 기능
function handleClickOutside(event) {
if (
colorPickRef.current &&
!colorPickRef.current.contains(event.target)
) {
// dom 바깥 클릭시 닫힘 상태 설정
setTimeout(function () {
setColorPickOpen(false);
}, 100);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [color1, color2, colorPickRef]);
return (
<>
<BtnButton type="button" onClick={() => setColorPickOpen(true)}>
색상 설정
</BtnButton>
{colorPickOpen && (
<Container ref={colorPickRef}>
<Title className="flexColumnCenter">색상 설정</Title>
<OptionContainer>
<BtnOption
className={` ${"flexColumnCenter"} ${
defaultSetting ? "active" : "nonActive"
}`}
onClick={() => handleDefault()}
>
기본 설정
</BtnOption>
<BtnOption
className={` ${"flexColumnCenter"} ${
defaultSetting ? "nonActive" : "active"
}`}
onClick={() => handleCustom()}
>
사용자 설정
</BtnOption>
</OptionContainer>
<ColorContainer>
<BtnColor1>
{defaultSetting ? null : (
<input
id="style1"
type="color"
defaultValue={color1}
onChange={(e) => setColor1(e.target.value)}
/>
)}
</BtnColor1>
{defaultSetting
? defaultColorPalette.map((color) => (
<ColorChip
key={color}
style={{
backgroundColor: color,
}}
/>
))
: colorPalette.map((color) => (
<ColorChip
key={color}
style={{
backgroundColor: color,
}}
/>
))}
<BtnColor2>
{defaultSetting ? null : (
<input
id="style2"
type="color"
defaultValue={color2}
onChange={(e) => setColor2(e.target.value)}
/>
)}
</BtnColor2>
</ColorContainer>
<BtnSubmit className="flexColumnCenter" onClick={handleSaveColor}>
확인
</BtnSubmit>
</Container>
)}
</>
);
};
참고 자료
'React' 카테고리의 다른 글
[React] Option에 JSON 객체를 value에 넣기 - JSON.stringify (0) | 2023.07.24 |
---|---|
[React] useState 리스트에서 index로 요소 제거하기 (0) | 2023.07.22 |
[React] useLocation을 활용해 사이드(메뉴)바를 현재 주소 기준으로 활성화하기 (0) | 2023.05.21 |
[React] 드롭다운(Dropdown) 컴포넌트 만들기 (0) | 2023.05.20 |
[React] SlideBar - 슬라이드 메뉴 만들기 (0) | 2023.05.11 |