Other answers here suggest using a texture. Here's a technique that doesn't use textures.
You want the boundaries between hexagons to be interesting. It's easier to make interesting boundaries when you move them into the center of what you're drawing. Instead of drawing the tiles directly, you draw the “dual” of the tile. This technique is called “corner tiles” (here and here and here). The dual of a hexagon is a triangle, so we would draw these triangles instead of the hexagons:

The boundaries between hexagons are now in the middle of the rendered triangles, so that'll let us do more interesting things with them. Bonus: you only need to draw two triangles per hexagon, instead of six (or twenty four as you are doing now).
Inside each of those triangles we want the fragment shader to draw the hexagons. We can do that with barycentric coordinates. Put (1,0,0), (0,1,0), and (0,0,1) at each vertex of the triangle. Inside the triangle, those coordinates will be interpolated. The fragment shader will receive (a,b,c) and can look to see which value is largest — that will tell us which of the three hexagons should be drawn at this point.
float max_n = max(barycentric.r, max(barycentric.g, barycentric.b));
if (max_n == barycentric.r) { color = v_color0; }
else if (max_n == barycentric.g) { color = v_color1; }
else if (max_n == barycentric.b) { color = v_color2; }
That's for straight lines.
If you want noisy edges, you can add noise to the barycentric coordinates:

By playing with the amplitude wavelength/frequency of noise, you can get some cool effects:

You need to be careful with the noise, making sure it's consistent across triangle boundaries. One way to do that is to pass in a hex id and use that as the seed value for each of the three noise values added to the barycentric coordinates.
I made an interactive demo here. (For the demo I didn't implement the hex id or some of the other things you might need if you were making this work in a real project — it's just a quick & dirty demo)