⬆️ 모바일에서도 사용해보세요!
loglog는 1년을 가볍게 회고할 수 있는 웹 서비스입니다. 여섯개의 키워드를 선택해 키워드에 해당하는 사진을 선택하거나 키워드에 대한 글을 작성할 수 있습니다. 당신의 한 해는 어떤 해였나요? 결과페이지에서 포인트 색상을 정해 색상을 입히고 이미지를 저장해 친구와 공유해보세요!
작년에 규리님과 이틀에 걸쳐 급하게 만들었던 연말 회고 페이지를 리뉴얼하기로 했다. 그동안 업데이트를 하고 싶었으나, 너무 바쁜 일상으로 인해,, 손 댈 엄두를 내지 못했는데 연말이 다가오고 규리님이 이제 슬슬 업데이트를 해볼까요 하고 제안을 주셔서 드디어 한을 풀 수 있게 되었다.
우선 프로젝트 이름을 변경했다. 기존 프로젝트 이름은 goodbye-2021
로 21년 회고에 포커싱되어있기 때문에, 년도에 상관없이 사용할 수 있도록 이름을 loglog
로 변경했다.
스펙은 이전과 동일하게 가져가되, 예전 기능에서 우리가 사용하면서 필요하다고 생각했던 여러 기능을 추가하기로 했다. 서버가 필요하지 않은 기능들이다보니 프론트 스택만을 사용했다.
김민주 |
이규리 |
메인 페이지 |
키워드 선택 페이지 |
결과 페이지 |
이번 업데이트는 다음과 같은 기능들이 추가되었다.
기존 페이지는 키워드 선택 페이지와 회고 입력 페이지가 분리되어, 만약 작성 중 키워드를 수정하고 싶다면 이전 페이지로 돌아가야했다. 유저 입장에서 사용하면서 키워드를 중간에 수정할 수 없음에 불편함을 느꼈고, 페이지를 통합하기로 했다.
키워드 선택 페이지 (전) |
회고 입력 페이지 (전) |
통합된 키워드 선택 페이지 (후) |
컬러를 수정할 수 없던 기존 버전과 달리 이번 버전에서부터는 컬러를 수정할 수 있기 때문에 전체 컬러를 블랙앤 화이트로 맞춰 유저가 선택한 어떤 컬러에도 어울릴 수 있도록 했다.
글의 길이에 따라 컨텍스트의 높이가 달라지는 레이아웃을 mansonry
라고 한다. (나도 이번에 처음 알게 되었다.) 대표적인 예시로 핀터레스트 UI가 있다.
이 레이아웃을 만들기 위해 처음에는 라이브러리를 사용해볼까 했으나 이미지의 경우 높이를 일정하게 해주는 예외사항이 있어 커스텀이 가능하도록 코드로 구현하는 방법을 찾아보았다.
export const MasonryGrid = ({ datas, pointColor }) => {
useEffect(() => {
masonryLayout();
}, []);
return (
<GridContainer className="masonry-container">
{datas?.map((data, index) => {
const { content, keyword } = data;
if (content)
return (
<AnswerBlock
key={keyword}
data={data}
data-type="text"
bgColor={getRandomColor(index, pointColor)}
/>
);
return <AnswerImage key={keyword} data={data} data-type="image" />;
})}
</GridContainer>
);
};
const GridContainer = styled.div`
display: grid;
box-sizing: border-box;
width: 100%;
grid-template-columns: repeat(2, 1fr);
gap: 5px;
grid-auto-rows: 5px;
`;
먼저 컨텍스트를 담을 컨테이너를 그리드로 만들어준다. 이때 그리드를 모눈종이처럼 촘촘이 나눠줘야하기 때문에 나는 5px로 높이를 설정해주었다. gap의 경우 역시 너무 간격이 크면 줄 길이에 따라 하단이 너무 길어지는 UI가 나오기 때문에 열 높이와 똑같이 5px로 설정해주었다.
이렇게 만들어진 grid에 각각의 컨텍스트가 길이에 따른 높이값을 가지도록 계산해준다.
export const masonryLayout = () => {
const container = document.querySelector(".masonry-container");
const masonryContainerStyle = getComputedStyle(container);
const autoRows = parseInt(
masonryContainerStyle.getPropertyValue("grid-auto-rows"),
);
document.querySelectorAll(".masonry-item").forEach((elt) => {
const scrollHeight = elt.scrollHeight;
elt.style.gridRowEnd = `span ${Math.ceil(scrollHeight / autoRows / 1.7)}`;
});
document.querySelectorAll(".masonry-item-image").forEach((elt) => {
const imgWidth = container.scrollWidth / 2 - 10;
elt.style.gridRowEnd = `span ${Math.ceil(imgWidth / autoRows / 2)}`;
});
};
각각이 가지게 될 높이를 열 높이로 나눠준다. 그렇게 나눠진 값만큼 grid의 높이를 차지하는 방식으로 계산하는데, 이 때 나는 하단이 너무 길어지는 현상이 생겨 1.7로 한번 더 나누어 주었다. 1.7이라는 깔끔하지 않은 숫자로 나눈 이유는.. 1과 2 사이에서 열심히 테스트해보다가 1.7이 가장 깔끔한 UI가 나와 1.7로 지정했다.
이미지
의 경우, 처음에는 1:1 비율로 지정해주고 싶어 처음에는 별다른 계산 없이 aspect-ratio
를 이용해 1:1 비율로 지정한 뒤, 넓이만큼 높이값을 가지도록 했으나 이런 경우 aspect-ratio의 특성에 의해 높이에 따라 넓이가 계산되고 넓이에 따라 높이가 계산되고가 반복되어 UI가 깨지는 문제가 발생했다. 어쩔 수 없이 aspect-ratio 설정을 빼고 글자와 똑같이 넓이값을 받아와 행 높이에 맞춰 나눠주도록 했다.
배포 후 가장 많은 요청을 받았던 컬러 선택 기능을 드디어 추가했다. 모든 칸의 컬러를 선택하는 방법도 있겠지만, 모든 컬러를 선택하도록 하는 것이 유저에게 피로감을 줄 수 있겠다는 생각을 해 포인트 컬러를 기반으로 명도가 변경되도록 수 정했다.
포인트 컬러 직접 선택 input의 경우, 기본 input을 이용했다. input의 타입을 color로 지정해주면, 유저가 컬러 피커를 이용할 수 있다.
크롬 컬러 피커 |
사파리 (웹) 컬러 피커 |
사파리 (모바일) 컬러 피커 |
기본 input 모양을 커스터마이징 하고 싶어 input의 display : none
처리 하고 label을 클릭하면 컬러 피커가 뜨도록 코드를 작성해주었다. 이렇게 코드를 작성했더니 크롬에서는 원하는대로 동작하지만 사파리에서는 동작하지않는 문제가 발생했다...!!😨
이 문제를 해결하기 위해 display 속성이 아닌 visibility : hidden
을 사용해보았으나 역시나 뜨지 않았고, opacity : 0
을 설정해주었더니 그제서야 비로소 동작했다.
const ColorInput = styled.input`
opacity: 0;
`;
컬러를 랜덤으로 설정하는 코드는 tinycolor2 라이브러리를 이용하였다! 해당 코드에 대한 설명은 글로 따로 적어두었으니 tinycolor2 라이브러리 사용 후기에서 확인하기!
해당 기능은 가장 반응이 좋았다! 😎
텍스트만 입력 가능하던 기존 버전과 달리 이번 버전에서는 이미지도 입력할 수 있도록 기능이 추가되었다!
데이터를 서버에 저장하거나 공유할 필요가 없어 이미지 서버를 따로 구축하진 않았고 blob 형태로 프로젝트 내에서 계속 사용하도록 했다.
const onFileChanged = (ev) => {
const {
target: { files },
} = ev;
if (files) {
const { length: FileLength } = files;
for (let i = 0; i < FileLength; i++) {
const reader = new FileReader();
reader.addEventListener("loadend", (event) => {
if (!event.target) return;
const { result } = event.target;
if (result) {
handleSaveImg(result);
}
});
reader.readAsDataURL(files[i]);
}
}
ev.target.value = "";
};
이미지 저장 기능은 라이브러리를 사용해 쉬울 것이라고 생각했으나, 예상외의 복병이 2가지 존재했다 🥲
유저가 추가한 이미지를 비율에 맞춰 보여주기 위해 처음에는 background에 이미지를 넣어주었다. 하지만 background로 이미지를 넣는 경우 이미지가 심하게 깨지는 문제가 있어 이를 해결하기 위해 image
태그에 넣는 것으로 변경했다.
image 태그에 넣어 이미지의 위치를 조절하기 위해 image 자체에 object-fit
를 넣어주었다. 웹 상에서는 원하는 대로 잘 나타났지만.. 이미지를 다운로드 받았더니 이미지가 확대되는 문제가 발생했다. (아놔)
사이트에서 살펴보니 object-fit
을 지원하고 있지 않고 있었다.
문제를 해결하기 위해 이미지 크기를 수동으로 조절하는 코드를 추가했다. 만약 이미지의 넓이가 높이보다 크다면 높이를 높이에 맞춰 이미지를 채우고, 높이가 넓이값보다 크다면 넓이에 맞춰 이미지를 채우는 방식을 사용했다.
useEffect(() => {
const myImgs = document.querySelectorAll(".my-images");
myImgs.forEach((myImg) => {
const imgWidth = myImg.clientWidth;
const imgHeight = myImg.clientHeight;
if (imgWidth > imgHeight) {
myImg.style.height = "100%";
} else {
myImg.style.width = "100%";
}
});
}, []);
어느정도 기능 구현이 완료된 이후, 주변에 베타 테스트를 부탁했다. 카카오톡을 이용해 링크를 전달했는데 문제가 발생했다. 인앱 브라우저에서는 이미지 다운로드가 불가능했던 것..! 앱이 자체 앱이 아니라 사파리나 크롬과 같은 브라우저로 이동시키는 코드를 추가 할 수 없었고, 웹 자체에 이를 대처하는 코드를 추가해야했다.
방법을 찾던 도중 다음의 블로그를 발견했다.
카카오, 네이버 인앱에서 외부 브라우저 띄우는 방법 정리
기존에는 링크 프로토콜 변경을 통해 ios에서 사파리가 열리도록 하는 편법을 썼으나 업데이트 이후로는 이 방법이 막혔다고 한다,, 현재로서는 유저가 직접 링크를 복사해서 이동하는 방법밖에 없어서 블로그에 나와있던 코드를 다음과 같이 수정했다.
const copyUrl = () => {
window.navigator.clipboard.writeText("https://loglog.co.kr").then(() => {
alert("링크가 복사되었습니다! 즐거운 한해 마무리 하세요~");
});
};
if (
navigator.userAgent.match(
/inapp|NAVER|KAKAOTALK|Snapchat|Line|WirtschaftsWoche|Thunderbird|Instagram|everytimeApp|WhatsApp|Electron|wadiz|AliApp|zumapp|iPhone(.*)Whale|Android(.*)Whale|kakaostory|band|twitter|DaumApps|DaumDevice\/mobile|FB_IAB|FB4A|FBAN|FBIOS|FBSS|SamsungBrowser\/[^1]/i,
)
) {
if (navigator.userAgent.match(/iPhone|iPad/i)) {
return (
<Container>
<Caution>
더 나은 환경을 위해 외부 브라우저로 이동해 이용해주세요! 🥲
</Caution>
<CopyButton type="button" onClick={copyUrl}>
링크 복사하기
</CopyButton>
</Container>
);
} else {
window.location.href =
"intent://" +
window.location.href.replace(/https?:\/\//i, "") +
"#Intent;scheme=http;package=com.android.chrome;end";
}
} else {
return (
<Container>
<Header>
안녕<span>2022</span>
</Header>
<Description>
<Strong>키워드</Strong>와 <Strong>색</Strong>으로
<br />
나의 2022 기록하기
</Description>
<Wrapper>
<BookImage src="/image/favorite-book.png" alt="" />
<FormName />
<Button toLink={"/answer"} children={"2022 정리하기"} />
</Wrapper>
</Container>
);
}
12월 15일을 데드라인으로 정했으나 생각치 못한 이슈들도 있고, 수정하고 싶은 부분들도 생겨 딜레이되었다..! 계속해서 수정사항들을 업데이트 할 예정이니 많관부!
대단하십니다 ㅎㅎ